「協程是輕量級的執行緒」,是不是經常聽到這樣的描述?這個描述對你理解協程有實質性的幫助嗎?可能沒有。閱讀本文,您會對「協程在 jvm 中實際的執行方式」,協程與執行緒的關係以及使用 jvm 執行緒模型時不可避免的「併發問題」有更多的了解。
// 在後台執行緒中計算第10個斐波那契數的協程
somescope.launch(dispatchers.default)
private fun synchronousfibonacci(n: long): long
請注意,因為沒有掛起(suspend),所以上面的**會在乙個執行緒中執行。如果將執行的邏輯轉移至不同的排程器(dispatcher),或者**塊可能在使用執行緒池的排程器中 yield / suspend,則協程可以在不同的執行緒中執行。
同樣,如果沒有協程,也可以使用執行緒手動執行上述邏輯,如下所示:
// 建立乙個四個執行緒的執行緒池
val executorservice = executors.newfixedthreadpool(4)
executorservice.execute
儘管手動管理執行緒池是可行的,但考慮到協程內建支援取消,更容易處理錯誤,使用可以降低記憶體洩露可能性的 結構化併發(structured concurrency) 以及 jetpack 庫的支援,「協程是 android 中非同步程式設計的推薦方案。」
「coroutinedispatcher 負責將協程的執行分發給 jvm 執行緒」。原理是:當使用coroutinedispatcher
時,它會使用 interceptcontinuation 攔截協程,該方法「將 continuation 包裝在 dispatchedcontinuation 中」。這是可行的,因為coroutinedispatcher
實現了 continuationinterceptor 介面。
如果需要在其它 dispatcher 中執行 continuation,dispatchedcontinuation
的 resumewith 方法負責分配給適合的協程!
此外,「dispatchedcontinuation」是dispatchedtask
,在 jvm 中它是可在 jvm 執行緒上執行的runnable
物件!這很酷不是嗎?當指定coroutinedispatcher
時,協程將轉換為dispatchedtask
,該dispatchedtask
會作為乙個runnable
在 jvm 執行緒上執行!
在建立協程時dispatch
方法是如何呼叫的呢?使用標準的協程構建器建立協程,可以指定協程以 coroutinestart 型別的start
引數。例如,您可以使用coroutinestart.lazy
將其配置為僅在需要時啟動。預設情況下,使用coroutinestart.default
來根據其coroutinedispatcher
排程協程執行。
您可以在 createdefaultdispatcher 方法中看到如何初始化dispatchers.default
。預設情況下使用 defaultscheduler。如果您檢視 dispatchers.io 的實現,它還將使用defaultscheduler
並允許根據需要建立至少 64 個執行緒。dispatchers.default
和dispatchers.io
隱式地連線在一起,因為它們使用相同的執行緒池。下面我們來看看使用不同的 dispatcher 呼叫withcontext
的執行時開銷是怎樣的?
預設情況下,coroutinescheduler 是 jvm 實現中使用的執行緒池,「它以最有效的方式將分派的協程分配給工作執行緒」。由於dispatchers.default
和dispatchers.io
使用相同的執行緒池,因此優化了它們之間的切換,以盡可能避免執行緒切換。協程庫可以優化這些呼叫,保留在相同的排程器(dispatcher)和執行緒上,並遵循乙個快速路徑(fast-path)。
由於不同執行緒上的排程工作非常簡單,協程「確實」使非同步程式設計更容易。另一方面,這種簡單性可能是一把雙刃劍:「由於協程執行在 jvm 執行緒模型上,它們不能簡單地擺脫執行緒模型帶來的併發問題。」因此,您必須注意避免併發問題。
多年來,不可變性(immutability)等良好實踐已經緩解了您可能遇到的一些與執行緒有關的問題。然而,有些場景下不適合不可變性。所有併發問題的根源在於狀態管理!特別是在多執行緒環境中訪問「可變狀態」。
多執行緒應用中的操作順序是不可**的。除了編譯優化會帶來有序性問題,上下文切換還可能帶來原子性問題(譯者注:併發問題可參考 譯者的筆記)。如果在訪問可變狀態時未採取必要的預防措施,則執行緒可能會看到過時的資料,丟失更新或遭受 競爭狀況 的困擾。
請注意,可變狀態和訪問順序的問題不是 jvm 特有的,這些問題也會影響其它平台的協程。這類問題並不罕見。例如可能乙個類需要將已登入使用者的資訊保留在記憶體中,或者在應用執行時快取某些值。如不小心,併發問題仍會在協程中發生!使用
withcontext(defaultdispatcher)
的掛起函式不能總是在同一執行緒中執行!
假設我們有乙個類可以快取使用者進行的交易。如果無法正確訪問快取,如下示例,則可能會發生併發錯誤:
class transactionsrepository(
private val defaultdispatcher: coroutinedispatcher = dispatchers.default
) else
}}
如何保護可變狀態或找到乙個好的 同步 策略,完全取決於資料的性質和所涉及的操作。本節旨在使您意識到可能會遇到的併發問題,而不是列出保護可變狀態的所有不同方法和 api。儘管如此,您還是可以從這裡獲得一些技巧和 api,以使得可變變數執行緒安全。
可變狀態應由乙個 class 封裝並擁有。該類集中對狀態的訪問,並根據場景使用更適合的同步策略來保護讀寫操作。
有一種解決方案是限制對乙個執行緒的讀/寫訪問。可以使用佇列以 生產者-消費者 的方式完成對可變狀態的訪問。jetbrains 對此有乙個很好的文件。
在 jvm 中,您可以使用執行緒安全的資料結構來保護可變變數。例如,對於簡單計數器,可以使用 atomicinteger。為了保護上面**的 map,可以使用 concurrenthashmap。concurrenthashmap 是乙個執行緒安全的同步集合,可優化 map 的讀寫吞吐量。
請注意,執行緒安全的資料結構不能防止呼叫方排序問題,它們只是確保記憶體訪問是原子性的。當邏輯不太複雜時,它們有助於避免使用鎖。例如,它們不能在上面顯示的transactioncache
示例中使用,因為操作順序和它們之間的邏輯需要執行緒和訪問保護。
同樣,這些執行緒安全資料結構中的資料必須是不可變的或受保護的,以防止在修改已儲存在其中的物件時出現競爭條件。
如果您有需要同步的復合操作,則@volatile
變數或執行緒安全的資料結構將無濟於事!內建的@synchronized
註解可能不夠精細,無法提高的效率。
在這種場景下,您可能需要使用併發工具(如 latch,訊號量 或 屏障)建立自己的同步機制。其它場景,您可以使用鎖或互斥鎖保護**的多執行緒訪問。
kotlin 中的 mutex 具有 lock 和 unlock 的掛起函式以用來手動保護協程**。mutex.withlock 擴充套件函式使用很簡單:
class transactionsrepository(
private val defaultdispatcher: coroutinedispatcher = dispatchers.default
) else }}
}
由於使用mutex
的協程在可以繼續執行前會暫停執行,因此它比阻塞執行緒的 jvm 鎖要有效得多。在協程中使用 jvm 同步類時要小心,因為這可能會阻塞在其中執行協程的執行緒並產生 liveness 問題。
傳遞給協程構建器的**塊最終在乙個或多個 jvm 執行緒上執行。因此,協程執行在 jvm 執行緒模型中並受其所有約束。使用協程,仍會寫出錯誤的多執行緒**。因此,在**中訪問共享的可變狀態要小心!
譯文完。
人總是喜歡做能夠獲得正反饋(成就感)的事情,如果感覺本文內容對你有幫助的話,麻煩點亮一下????,這對我很重要哦~
Kotlin 協程輕量 協程與執行緒對比
本例使用協程和執行緒兩個方式執行一段任務 協程 任務是每秒列印出兩個 執行100 000個任務 test fun testmet runblocking println val end system.currenttimemillis println end time end println 耗時 ...
Kotlin協程筆記
會阻塞主線程,等待協程執行完,才會繼續執行主線程 不會阻塞主線程,返回job型別的物件 var job globalscope.launch 3 async 用於啟動乙個非同步協程任務,與launch用法基本一樣,不阻塞執行緒,區別在於 async的返回值是deferred,將最後乙個封裝成了該物件...
Kotlin協程快速入門
協程,全稱可以譯作協同程式,很多語言都有這個概念和具體實現,之前入門python的時候接觸過,而kotlin其實也早就有這個擴充套件功能庫了,只不過之前一直處於實驗階段,不過前段時間1.0的正式版終於出了,網上的相關部落格也多了起來,經過這幾天的學習我也來做下小結吧。首先貼下kotlin協程的官方g...