- 協程間的數據傳遞:單值傳遞和多值傳遞(數據流的傳遞)
- 單值傳遞:
- launch創建的協程:
- 多協程是順序執行,且各個協程的函數體內沒有掛起當前協程的邏輯(包括延時邏輯),各個協程的執行順序是順序執行,則下一個協程可以使用上一個協程的返回值。
- 根協程聲明值,子協程修改值,或者延遲產生數據實現在協程之間傳遞值,不過需要注意的是分析根協程和各個協程的邏輯的執行順序,對數據的修改順序會影響到數據的狀態進而也就影響了協程對數據的使用。
- async創建的協程:
- 協程并發,此時不建議協程間數據傳遞使用,個人理解的此時的使用場景是協程創建自己的數據而所有協程執行完成匯總協程產生的數據進行使用,使用其并發縮短了邏輯的執行時間。
- 數據流的傳遞:
- 通過通道(channel)進行協程間數據流的傳遞。
- 阻塞隊列(BlockingQueue):項目中常見的模型(生產者消費者模型)常用的數據結構,提供put和take且都是阻塞的,即take的時候若沒有數據則會阻塞線程直到有了數據,同樣put也是沒有空間則阻塞線程等到數據take隊列有了空間。
-
協程中的通道等同于阻塞隊列,用于協程間數據流的傳遞,提供send(數據的發送)和receive(數據的接受)和隊列不同的是不會阻塞。
通道數據傳遞-
和隊列不同,通道提供了close關閉邏輯,即通道關閉后則不會再有數據發送,close操作類似于向通道發送一個特殊的關閉標記. 收到這個關閉標記之后, 對通道的迭代操作將會立即停止, 因此可以保證在關閉操作以前發送的所有數據都會被正確接收。
通道關閉 - 構造通道的生產者(producer):在協程中產生一個數值序列, 這是很常見的模式. 這是并發代碼中經常出現的 生產者(producer)/消費者(consumer) 模式的一部分. 你可以將生產者抽象為一個函數, 并將通道作為函數的參數, 然后向通道發送你生產出來的值, 但這就違反了通常的函數設計原則, 也就是函數的結果應該以返回值的形式對外提供.
通道Produce
有一個便利的協程構建器, 名為 produce, 它可以很簡單地編寫出生產者端的正確代碼, 還有一個擴展函數 consumeEach, 可以在消費者端代碼中替代for
循環:即:
通道produce封裝及其函數consumeEach- 帶緩沖區的通道:
- 通道分為發送和接受兩個場景,通常的通道如果沒有接受僅有發送的場景則是將發送協程掛起,等待接受協程的調用時才會執行發送的邏輯進行數據的發送,同樣 先執行接受協程也會將接受協程掛起等待發送協程的調用然后重新執行接受協程。
- 但是對于帶緩沖區的通道則是在數據的發送提供一個緩沖區,即通道的參數指定發送數據的緩存個數,此時通道沒有接受的場景,在僅有的發送場景的時候也會發送緩沖區的數據,達到緩沖區的指定數后將發送數據協程掛起。
- Channel() 工廠函數和 produce 構建器都可以接受一個可選的
capacity
參數, 用來指定 緩沖區大小. 緩沖區可以允許發送者在掛起之前發送多個數據, 類似于指定了容量的BlockingQueue
, 它會在緩沖區已滿的時候發生阻塞.
緩沖區通道
- 帶緩沖區的通道:
-
定時器通道:類似于timer定時任務,即根據指定的定時時間發送一個unit的定時值,用戶可以根據定時值處理一些常見的定時邏輯,可以使用 ticker 工廠函數來創建這種通道. 使用通道的 ReceiveChannel.cancel 方法來指出不再需要它繼續產生數據了。即:
定時器通道
-
- 通過通道(channel)進行協程間數據流的傳遞。
- 多協程訪問通道:
- 多協程接受通道的數據:
- 通道里的數據可以被多個協程收到,通道的數據的發送不會發生變化,接受數據的協程不固定
- 取消通道的生成者協程,則通道會被關閉,進而接受通道數據的協程也都會被取消。
-
在接受通道數據的協程的函數體中for循環和consumeEach的不同點是:前面for循環若出現異常并不會取消接受數據的協程因為豈不是掛起函數,也就不會取消其他的接受通道數據的協程,但是后者是一個掛起函數若出現異常則會取消協程,進而影響到通道的關閉,其他接受者協程和生產者協程也就取消。
生產者協程
處理數據協程
啟動多個數據的協程
- 多協程向通道發送數據:
- 發射數據也可以多個協程向其寫入數據,數據的順序取決于協程的寫入邏輯。
-
取消所有的協程則通道的數據也就隨之取消。
通道數據生產者
多協程寫入數據
-
如果從多個協程中調用通道的發送和接收操作, 從調用發生的順序來看, 這些操作是 平等的. 通道對這些方法以先進先出(first-in first-out)的順序進行服務, 也就是說, 第一個調用 receive 的協程會得到通道中的數據. 在下面的示例程序中, 有兩個 "ping" 和 "pong" 協程, 從公用的一個 "table" 通道接收 "ball" 對象.
多協程訪問通道的順序
- 多協程接受通道的數據:
- 管道:上面講了多協程生產數據和多協程接收數據,在這個場景中可以講這些協程串聯起來形成一個數據處理的管道。
- ReceiveChannel:通道的數據接受的封裝對象,producer產生數據后返回的對象即時這個對象,封裝了產生的數據(具體可以參考上面的代碼)。
-
管道的協程串聯即是通過上面的這個對象進行串聯,即中間協程可以接受生產者產生的數據修改后再封裝返回,最終到最后一個協程通過其接受者獲取到數據。即:
管道生產者
管道數據中間處理
管道的串聯
- launch創建的協程:
- 單值傳遞:
- 多線程協程的數據的同步:
問題:-
通過Default線程指定,多協程的執行會在多線程中執行,此時就會出現多線程共享值的問題,即多個線程同時訪問并修改同一個值就會出現意想不到的問題。
共享值
多線程
備注:多線程中多協程的原因導致并沒有出現100*1000的結果值。
-
- 多協程的多線程執行的解決和多線程共享值基本一樣,即:
-
針對簡單的基礎變量,使用volatile(即使用其原子性:針對變量的每一個線程的讀和寫保證其可見性)不能保證并發問題解決,對于上面的案例使用這個關鍵字并不能保證每次都是100*1000,即:
并發案例 - 下面幾種方法可以保證多線程對共享值的修改的線程安全。
-
和多線程處理一致數據結構使用線程安全的數據結構,即使用線程安全的 (也叫 同步的(synchronized), 線性的(linearizable), 或者 原子化的(atomic)) 數據結構, 這些數據結構會對需要在共享的狀態數據上進行的操作提供必要的同步保障. 在我們的簡單的計數器示例中, 可以使用 AtomicInteger 類, 它有一個原子化的 incrementAndGet 操作且對于這個具體的問題, 這是最快的解決方案. 這種方案適用于計數器, 集合, 隊列, 以及其他標準數據結構, 以及這些數據結構的基本操作. 但是, 這種方案并不能簡單地應用于復雜的狀態變量, 或者那些沒有現成的線程安全實現的復雜操作.
線程安全的數據結構 - 細粒度的線程限定:將涉及到的共享值轉變的lambda表達式或者匿名函數放到獨一線程中去,即對于共享值的修改放到唯一線程中去修改。
-
缺點是:代碼的執行變慢了,因為在執行中要不停的進行線程的切換,即協程線程和修改數據的線程來回切換。
單線程 -
粗粒度的線程限定:即將整個操作放到一個獨立線程中去這樣比細粒度線程限定速度快又能保證共享值的線程安全。
單線程線程安全- 加鎖同步即和多線程中的鎖同步一致,不過協程的語法和線程不一致,協程是通過Mutex實現的它的 lock和 unlock 函數可以用來界定臨界區. 主要的區別在于
Mutex.lock()
是一個掛起函數. 它不會阻塞線程.
* 還有一個擴展函數 withLock, 它用非常便利的方式實現mutex.lock(); try { ... } finally { mutex.unlock() }
模式:
加鎖實現同步
- 加鎖同步即和多線程中的鎖同步一致,不過協程的語法和線程不一致,協程是通過Mutex實現的它的 lock和 unlock 函數可以用來界定臨界區. 主要的區別在于
-
-
-
- select選擇:上面通道中介紹了多個掛起函數發送值的場景,使用select選擇語法可以在多個掛起函數中選擇第一個執行完畢的結果,其他掛起函數取消或者關閉。
-
通道中選擇使用:
掛起函數1
掛起函數2
通道選擇掛起函數
測試代碼及其結果 -
通道的關閉會導致通道的select選擇的onReceive函數語句失敗且拋出對應的異常,對于此可以使用函數onReceiveCatching對其進行抓取并處理其異常。即:
選擇處理異常掛起函數
生產者掛起函數及其測試掛起函數和結果
備注:通道上選擇的優先級:
- 多個通道且每個通道的數據發送有先后順序:此時取決于數據的發送順序,例如上面的第一個案例,第一個通道500ms發送一個數據所以第一個數據是第一個通道,且第一個通道的數據居多,偶爾第二個通道也會有數據。
- 多個通道且每個通道沒有具體的數據發送順序區分:比如第二個案例:優先使用第一個通道,后續會根據通道的數據發送順序獲取數據。
- 通道的數據發送時選擇發送的通道:上面介紹了在接受數據的時候多通道的選擇,此處介紹的是發送數據發送到多個通道:選擇表達式也可以使用 onSend 子句, 它可以與選擇表達式的偏向性結合起來, 起到很好的作用。
- onSend函數可以指定要把數據發送到那一個通道,指定語法使用通道調用即可,若不指定則是發送到當前的生產者的主通道中去。
onSend函數
![測試及其結果](https://upload-images.jianshu.io/upload_images/1346105-347add8547d98405.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240
- onSend函數可以指定要把數據發送到那一個通道,指定語法使用通道調用即可,若不指定則是發送到當前的生產者的主通道中去。
-