您好,登錄后才能下訂單哦!
這篇文章主要講解了“為什么放棄使用Kotlin中的協程”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“為什么放棄使用Kotlin中的協程”吧!
調試
請看下面一段代碼。
suspend fun retrieveData(): SomeData { val request = createRequest() val response = remoteCall(request) return postProcess(response) } private suspend fun remoteCall(request: Request): Response { // do suspending REST call }
假設我們要調試 retrieveData 函數,可以在第一行中放置一個斷點。然后啟動調試器(我使用的是 IntelliJ),它在斷點處停止。現在我們執行一個 Step Over(跳過調用 createRequest),這也正常。但是如果再次 Step Over,程序就會直接運行,調用 remoteCall() 之后不會停止。
為什么會這樣?JVM 調試器被綁定到一個 Thread 對象上。當然,這是一個非常合理的選擇。然而,當引入協程之后,一個線程不再做一件事。仔細一看:remoteCall(request) 調用的是一個 suspend 函數,雖然我們在調用它的時候并沒有在語法中看到它。那么會發生什么?我們執行調試器 "step over ",調試器運行 remoteCall 的代碼并等待。
這就是難點所在:當前線程(我們的調試器被綁定到該線程)只是我們的coroutine 的執行者。當我們調用 suspend 函數時,會發生的情況是,在某個時刻,suspend 函數會 yield。這意味著另外一個 Thread 將繼續執行我們的方法。我們有效地欺騙了調試器。
我發現的唯一的解決方法是在我想執行的行上放置一個斷點,而不是使用Step Over。不用說,這是個大麻煩。而且很顯然,這不僅僅是我一個人的問題。
此外,在一般的調試中,很難確定一個單一的 coroutine 當前在做什么,因為它在線程之間跳躍。當然,coroutine 是有名字的,你可以在日志中不僅打印線程,還可以打印 coroutine 的名字,但根據我的經驗,調試基于 coroutine 的代碼所需的心智負擔,要比基于線程的代碼高很多。
REST 調用中綁定 context 數據
在微服務上開發,一個常見的設計模式是,接收一個某種形式認證的 REST 調用,并將相同的認證傳遞給其他微服務的所有內部調用。在最簡單的情況下,我們至少要保留調用者的用戶名。
然而,如果這些對其他微服務的調用在我們調用棧中嵌套了 10 層深度怎么辦?我們當然不希望在每個函數中都傳遞一個認證對象作為參數。我們需要某種形式的 "context",這種 context 是隱性存在的。
在傳統的基于線程的框架中,如 Spring,解決這個問題的方法是使用 ThreadLocal 對象。這使得我們可以將任何一種數據綁定到當前線程。只要一個線程對應一個 REST 調用(你應該始終以這個為目標),這正是我們需要的。這個模式的很好的例子是 Spring 的 SecurityContextHolder。
對于 coroutine,情況就不同了。一個 ThreadLocal 不再對應一個協程,因為你的工作負載會從一個線程跳到另一個線程;不再是一個線程在其整個生命周期內伴隨一個請求。在 Kotlin coroutine 中,有 CoroutineContext。本質上,它不過是一個 HashMap,與 coroutine 一起攜帶(無論它運行在哪個線程上)。它有一個可怕的過度設計的 API,使用起來很麻煩,但這不是這里的主要問題。
真正的問題是,coroutine 不會自動繼承上下文。
例如:
suspend fun sum(): Int { val jobs = mutableListOf<Deferred<Int>>() for(child in children){ jobs += async { // we lose our context here! child.evaluate() } } return jobs.awaitAll().sum() }
每當你調用一個 coroutine builder,如 async、runBlocking 或 launch,你將(默認情況下)失去你當前的 coroutine 上下文。你可以通過將上下文顯式地傳遞到 builder 方法中來避免這種情況,但是上帝保佑你不要忘記這樣做(編譯器不會管這些!)。
一個子 coroutine 可以從一個空的上下文開始,如果有一個上下文元素的請求進來,但沒有找到任何東西,可以向父 coroutine 上下文請求該元素。然而,在 Kotlin 中不會發生這種情況,開發人員需要手動完成,每一次都是如此。
如果你對這個問題的細節感興趣,我建議你看看這篇博文。
https://blog.tpersson.io/2018/04/22/emulating-request-scoped-objects-with-kotlin-coroutines/
synchronized 不再如你想的那樣工作
在 Java 中處理鎖或 synchronized 同步塊時,我考慮的語義通常是 "當我在這個塊中執行時,其他調用不能進入"。當然“其他調用”意味著存在某種身份,在這里就是線程,這應該在你的腦海中升起一個大紅色的警告信號。
看看下面的例子。
val lock = ReentrantLock() suspend fun doWithLock(){ lock.withLock { callSuspendingFunction() } }
這個調用很危險,即使 callSuspendingFunction() 沒有做任何危險的操作,代碼也不會像你想象的那樣工作。
進入同步鎖
調用 suspend 功能
協程 yield,當前線程仍然持有鎖。
另一個線程繼續我們的 coroutine
還是同一個協程,但我們不再是鎖的 owner 了!
潛在的沖突、死鎖或其他不安全的情況數量驚人。你可能會說,我們只是需要設計我們的代碼來處理這個問題。我同意,然而我們談論的是 JVM。那里有一個龐大的 Java 庫生態。而它們并沒有做好處理這些情況的準備。
這里的結果是:當你開始使用 coroutine 的時候,你就放棄了使用很多 Java 庫的可能性,因為它們目前只能工作在基于線程的環境。
單機吞吐量與水平擴展
對于服務器端來說,coroutine 的一大優勢是,一個線程可以處理更多的請求;當一個請求等待數據庫響應時,同一個線程可以愉快地服務另一個請求。特別是對于 I/O 密集型任務,這可以提高吞吐量。
然而,正如這篇博文所希望向您展示的那樣,在許多層面上,使用 coroutine 都有一個非零成本的開銷。
由此產生的問題是:這個收益是否值得這個成本?而在我看來,答案是否定的。在云和微服務環境中,有一些現成的擴展機制,無論是 Google AppEngine、AWS Beanstalk 還是某種形式的 Kubernetes。如果當前負載增加,這些技術將簡單地按需生成你的微服務的新實例。因此,考慮到引入 coroutine 帶來的額外成本,單一實例所能處理的吞吐量就不那么重要了。這就降低了我們使用 coroutine 所獲得的價值。
Coroutine 有其存在的價值
話說回來,Coroutine 還是有其使用場景。當開發只有一個 UI 線程的客戶端 UI 時,coroutine 可以幫助改善你的代碼結構,同時符合 UI 框架的要求。聽說這個在安卓系統上很好用。Coroutine 是一個有趣的主題,然而對于服務器端開發來說,我覺得協程還差點意思。JVM 開發團隊目前正在開發 Fiber,本質上也是 coroutine,但他們的目標是與 JVM 基礎庫更好共存。這將是有趣的,看它將來如何發展,以及 Jetbrains 對 Kotlin coroutine 對此會有什么反應。在最好的情況下,Kotlin coroutine 將來只是簡單映射到 Fiber 上,而調試器也能足夠聰明來正確處理它們。
感謝各位的閱讀,以上就是“為什么放棄使用Kotlin中的協程”的內容了,經過本文的學習后,相信大家對為什么放棄使用Kotlin中的協程這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。