<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【譯】Kotlin 協(xié)程,JVM 線程以及并發(fā)問(wèn)題

          共 4021字,需瀏覽 9分鐘

           ·

          2021-02-06 19:21

          原文:Bridging the gap between coroutines, JVM threads, and concurrency problems
          作者:Manuel Vivo
          譯者:Flywith24

          「協(xié)程是輕量級(jí)的線程」,是不是經(jīng)常聽到這樣的描述?這個(gè)描述對(duì)你理解協(xié)程有實(shí)質(zhì)性的幫助嗎?可能沒(méi)有。閱讀本文,您會(huì)對(duì) 「協(xié)程在 JVM 中實(shí)際的執(zhí)行方式」,協(xié)程與線程的關(guān)系以及使用 JVM 線程模型時(shí)不可避免的 「并發(fā)問(wèn)題」 有更多的了解。

          協(xié)程與 JVM 線程

          協(xié)程旨在簡(jiǎn)化執(zhí)行異步操作的代碼。基于 JVM 的協(xié)程的本質(zhì)是:「?jìng)鬟f給協(xié)程構(gòu)建器的 lambda 代碼塊最終會(huì)在特定的 JVM 線程上執(zhí)行」。如下面這個(gè)簡(jiǎn)單的 斐波那契數(shù)列(譯者注:鏈接已改為百度百科)的計(jì)算:

          //?在后臺(tái)線程中計(jì)算第10個(gè)斐波那契數(shù)的協(xié)程
          someScope.launch(Dispatchers.Default)?{
          ????val?fibonacci10?=?synchronousFibonacci(10)
          ????saveFibonacciInMemory(10,?fibonacci10)
          }
          private?fun?synchronousFibonacci(n:?Long):?Long?{?/*?...?*/?}

          上面的 異步 協(xié)程代碼塊執(zhí)行了同步且阻塞的斐波那契計(jì)算并將結(jié)果保存至內(nèi)存。該代碼塊被 「協(xié)程庫(kù)管理的線程池(通過(guò) Dispatchers.Default 配置)分發(fā)調(diào)度」 并且在未來(lái)的某個(gè)時(shí)刻(取決于線程池的策略)在線程池中的線程執(zhí)行。

          請(qǐng)注意,因?yàn)闆](méi)有掛起(suspend),所以上面的代碼會(huì)在一個(gè)線程中執(zhí)行。如果將執(zhí)行的邏輯轉(zhuǎn)移至不同的調(diào)度器(dispatcher),或者代碼塊可能在使用線程池的調(diào)度器中 yield / suspend,則協(xié)程可以在不同的線程中執(zhí)行。

          同樣,如果沒(méi)有協(xié)程,也可以使用線程手動(dòng)執(zhí)行上述邏輯,如下所示:

          //?創(chuàng)建一個(gè)四個(gè)線程的線程池
          val?executorService?=?Executors.newFixedThreadPool(4)
          //?在線程池中的線程上調(diào)度并執(zhí)行下面代碼
          executorService.execute?{
          ????val?fibonacci10?=?synchronousFibonacci(10)
          ????saveFibonacciInMemory(10,?fibonacci10)
          }

          盡管手動(dòng)管理線程池是可行的,但考慮到協(xié)程內(nèi)置支持取消,更容易處理錯(cuò)誤,使用可以降低內(nèi)存泄露可能性的 結(jié)構(gòu)化并發(fā)(structured concurrency) 以及 Jetpack 庫(kù)的支持,「協(xié)程是 Android 中異步編程的推薦方案。」

          背后的原理

          開始創(chuàng)建協(xié)程到在線程中執(zhí)行,這過(guò)程發(fā)生了什么?當(dāng)使用標(biāo)準(zhǔn)的協(xié)程構(gòu)建器創(chuàng)建協(xié)程時(shí),您可以指定在特定的 CoroutineDispatcher 執(zhí)行代碼,默認(rèn)將使用 Dispatchers.Default

          「CoroutineDispatcher 負(fù)責(zé)將協(xié)程的執(zhí)行分發(fā)給 JVM 線程」。原理是:當(dāng)使用 CoroutineDispatcher 時(shí),它會(huì)使用 interceptContinuation 攔截協(xié)程,該方法 「將 Continuation 包裝在 DispatchedContinuation 中」。這是可行的,因?yàn)?CoroutineDispatcher 實(shí)現(xiàn)了 ContinuationInterceptor 接口。

          如果您閱讀過(guò)我的 協(xié)程工作原理 的文章,您已經(jīng)知道編譯器創(chuàng)建一個(gè)狀態(tài)機(jī),狀態(tài)機(jī)的信息(如下一步需要執(zhí)行的內(nèi)容)保存在 ?Continuation 對(duì)象中。

          如果需要在其它 Dispatcher 中執(zhí)行 Continuation,DispatchedContinuation 的 resumeWith 方法負(fù)責(zé)分配給適合的協(xié)程!

          此外,「DispatchedContinuation」DispatchedTask,在 JVM 中它是可在 JVM 線程上運(yùn)行的 Runnable 對(duì)象!這很酷不是嗎?當(dāng)指定 CoroutineDispatcher 時(shí),協(xié)程將轉(zhuǎn)換為 DispatchedTask,該DispatchedTask 會(huì)作為一個(gè) Runnable 在 JVM 線程上執(zhí)行!

          在創(chuàng)建協(xié)程時(shí) dispatch 方法是如何調(diào)用的呢?使用標(biāo)準(zhǔn)的協(xié)程構(gòu)建器創(chuàng)建協(xié)程,可以指定協(xié)程以 CoroutineStart 類型的 start 參數(shù)。例如,您可以使用 CoroutineStart.LAZY 將其配置為僅在需要時(shí)啟動(dòng)。默認(rèn)情況下,使用 CoroutineStart.DEFAULT 來(lái)根據(jù)其 CoroutineDispatcher 調(diào)度協(xié)程執(zhí)行。

          協(xié)程中的代碼塊最終如何在線程中執(zhí)行的圖示

          協(xié)程中的代碼塊最終如何在線程中執(zhí)行的圖示

          調(diào)度器與線程池

          您可以使用 Executor.asCoroutineDispatcher() 擴(kuò)展函數(shù)將協(xié)程轉(zhuǎn)換為 CoroutineDispatcher,從而在您的 app 線程池中執(zhí)行協(xié)程。您也可以使用協(xié)程庫(kù)中的默認(rèn) Dispatchers。

          您可以在 createDefaultDispatcher 方法中看到如何初始化 Dispatchers.Default。默認(rèn)情況下使用 DefaultScheduler。如果您查看 Dispatchers.IO 的實(shí)現(xiàn),它還將使用 DefaultScheduler 并允許根據(jù)需要?jiǎng)?chuàng)建至少 64 個(gè)線程。Dispatchers.DefaultDispatchers.IO 隱式地連接在一起,因?yàn)樗鼈兪褂孟嗤木€程池。下面我們來(lái)看看使用不同的 Dispatcher 調(diào)用 withContext 的運(yùn)行時(shí)開銷是怎樣的?

          線程與 withContext 性能

          在 JVM 中,如果創(chuàng)建的線程多于可用的 CPU 核心數(shù),則在線程之間進(jìn)行切換會(huì)帶來(lái)一些運(yùn)行時(shí)開銷。上下文切換 的成本并不低!操作系統(tǒng)需要保存和恢復(fù)執(zhí)行上下文,CPU 需要花時(shí)間調(diào)度線程而不是運(yùn)行實(shí)際的 app 工作。除此之外,如果線程正在運(yùn)行的代碼阻塞了,也可能會(huì)發(fā)生上下文切換。如果線程是這種情況,將 withContext 與不同的 Dispatchers 配合使用是否會(huì)對(duì)性能造成損失?

          幸運(yùn)的是,如您所料,線程池為我們管理了這些復(fù)雜的場(chǎng)景,并嘗試盡可能優(yōu)化被執(zhí)行的工作(這就是在線程池上執(zhí)行工作比手動(dòng)在線程中執(zhí)行工作更好的原因)。協(xié)程也從中受益(因?yàn)樗鼈兪窃诰€程池中調(diào)度的)!最重要的是,協(xié)程不阻塞線程,而是 suspend 工作!甚至更有效率!

          默認(rèn)情況下,CoroutineScheduler 是 JVM 實(shí)現(xiàn)中使用的線程池,「它以最有效的方式將分派的協(xié)程分配給工作線程」。由于 Dispatchers.DefaultDispatchers.IO 使用相同的線程池,因此優(yōu)化了它們之間的切換,以盡可能避免線程切換。協(xié)程庫(kù)可以優(yōu)化這些調(diào)用,保留在相同的調(diào)度器(dispatcher)和線程上,并遵循一個(gè)快速路徑(fast-path)。

          由于 Dispatchers.Main 通常是 UI app 中不同的線程,因此在協(xié)程中 Dispatchers.DefaultDispatchers.Main 之間切換不會(huì)帶來(lái)巨大的性能成本,因?yàn)閰f(xié)程只是掛起(即停止在一個(gè)線程中執(zhí)行),并被調(diào)度到在另一個(gè)線程中執(zhí)行。

          協(xié)程中的并發(fā)問(wèn)題

          由于不同線程上的調(diào)度工作非常簡(jiǎn)單,協(xié)程 「確實(shí)」 使異步編程更容易。另一方面,這種簡(jiǎn)單性可能是一把雙刃劍:「由于協(xié)程運(yùn)行在 JVM 線程模型上,它們不能簡(jiǎn)單地?cái)[脫線程模型帶來(lái)的并發(fā)問(wèn)題。」 因此,您必須注意避免并發(fā)問(wèn)題。

          多年來(lái),不可變性(immutability)等良好實(shí)踐已經(jīng)緩解了您可能遇到的一些與線程有關(guān)的問(wèn)題。然而,有些場(chǎng)景下不適合不可變性。所有并發(fā)問(wèn)題的根源在于狀態(tài)管理!特別是在多線程環(huán)境中訪問(wèn) 「可變狀態(tài)」

          多線程應(yīng)用中的操作順序是不可預(yù)測(cè)的。除了編譯優(yōu)化會(huì)帶來(lái)有序性問(wèn)題,上下文切換還可能帶來(lái)原子性問(wèn)題(譯者注:并發(fā)問(wèn)題可參考 譯者的筆記)。如果在訪問(wèn)可變狀態(tài)時(shí)未采取必要的預(yù)防措施,則線程可能會(huì)看到過(guò)時(shí)的數(shù)據(jù),丟失更新或遭受 競(jìng)爭(zhēng)狀況 的困擾。

          請(qǐng)注意,可變狀態(tài)和訪問(wèn)順序的問(wèn)題不是 JVM 特有的,這些問(wèn)題也會(huì)影響其它平臺(tái)的協(xié)程。

          使用協(xié)程的 app 本質(zhì)上是一個(gè)多線程 app。「使用協(xié)程并且包含可變狀態(tài)的類必須采取預(yù)防措施以確保執(zhí)行結(jié)果符合預(yù)期」,即確保在協(xié)程中執(zhí)行的代碼能看到最新版本的數(shù)據(jù)。這樣,不同的線程不會(huì)互相干擾。并發(fā)問(wèn)題可能會(huì)導(dǎo)致非常小的錯(cuò)誤,難以調(diào)試,甚至是 heisenbug!

          這類問(wèn)題并不罕見。例如可能一個(gè)類需要將已登錄用戶的信息保留在內(nèi)存中,或者在應(yīng)用運(yùn)行時(shí)緩存某些值。如不小心,并發(fā)問(wèn)題仍會(huì)在協(xié)程中發(fā)生!使用 withContext(defaultDispatcher) 的掛起函數(shù)不能總是在同一線程中執(zhí)行!

          假設(shè)我們有一個(gè)類可以緩存用戶進(jìn)行的交易。如果無(wú)法正確訪問(wèn)緩存,如下示例,則可能會(huì)發(fā)生并發(fā)錯(cuò)誤:

          class?TransactionsRepository(
          ??private?val?defaultDispatcher:?CoroutineDispatcher?=?Dispatchers.Default
          )?{

          ??private?val?transactionsCache?=?mutableMapOf()

          ??private?suspend?fun?addTransaction(user:?User,?transaction:?Transaction)?=
          ????//?小心!訪問(wèn)緩存是不受保護(hù)的。
          ????//?并發(fā)錯(cuò)誤可能發(fā)生:線程可以看到過(guò)時(shí)的數(shù)據(jù),競(jìng)爭(zhēng)條件可能發(fā)生
          ????withContext(defaultDispatcher)?{
          ??????if?(transactionsCache.contains(user))?{
          ????????val?oldList?=?transactionsCache[user]
          ????????val?newList?=?oldList!!.toMutableList()
          ????????newList.add(transaction)
          ????????transactionsCache.put(user,?newList)
          ??????}?else?{
          ????????transactionsCache.put(user,?listOf(transaction))
          ??????}
          ????}
          }

          即使我們討論的是 Kotlin,《Java 并發(fā)編程實(shí)踐》(作者:Brian Goetz)一書也是了解更多這部分內(nèi)容和 JVM 系統(tǒng)并發(fā)問(wèn)題的絕佳資源。或者參考 Jetbrains 關(guān)于 共享可變狀態(tài)和并發(fā) 的文檔。

          保護(hù)可變狀態(tài)

          如何保護(hù)可變狀態(tài)或找到一個(gè)好的 同步 策略,完全取決于數(shù)據(jù)的性質(zhì)和所涉及的操作。本節(jié)旨在使您意識(shí)到可能會(huì)遇到的并發(fā)問(wèn)題,而不是列出保護(hù)可變狀態(tài)的所有不同方法和 API。盡管如此,您還是可以從這里獲得一些技巧和 API,以使得可變變量線程安全。

          封裝

          可變狀態(tài)應(yīng)由一個(gè) class 封裝并擁有。該類集中對(duì)狀態(tài)的訪問(wèn),并根據(jù)場(chǎng)景使用更適合的同步策略來(lái)保護(hù)讀寫操作。

          線程約束

          有一種解決方案是限制對(duì)一個(gè)線程的讀/寫訪問(wèn)。可以使用隊(duì)列以 生產(chǎn)者-消費(fèi)者 的方式完成對(duì)可變狀態(tài)的訪問(wèn)。JetBrains 對(duì)此有一個(gè)很好的文檔。

          不要重復(fù)造輪子

          在 JVM 中,您可以使用線程安全的數(shù)據(jù)結(jié)構(gòu)來(lái)保護(hù)可變變量。例如,對(duì)于簡(jiǎn)單計(jì)數(shù)器,可以使用 AtomicInteger。為了保護(hù)上面代碼的 Map,可以使用 ConcurrentHashMap。ConcurrentHashMap 是一個(gè)線程安全的同步集合,可優(yōu)化 Map 的讀寫吞吐量。

          請(qǐng)注意,線程安全的數(shù)據(jù)結(jié)構(gòu)不能防止調(diào)用方排序問(wèn)題,它們只是確保內(nèi)存訪問(wèn)是原子性的。當(dāng)邏輯不太復(fù)雜時(shí),它們有助于避免使用鎖。例如,它們不能在上面顯示的 transactionCache 示例中使用,因?yàn)椴僮黜樞蚝退鼈冎g的邏輯需要線程和訪問(wèn)保護(hù)。

          同樣,這些線程安全數(shù)據(jù)結(jié)構(gòu)中的數(shù)據(jù)必須是不可變的或受保護(hù)的,以防止在修改已存儲(chǔ)在其中的對(duì)象時(shí)出現(xiàn)競(jìng)爭(zhēng)條件。

          自定義解決方案

          如果您有需要同步的復(fù)合操作,則 @Volatile 變量或線程安全的數(shù)據(jù)結(jié)構(gòu)將無(wú)濟(jì)于事!內(nèi)置的 @Synchronized 注解可能不夠精細(xì),無(wú)法提高的效率。

          在這種場(chǎng)景下,您可能需要使用并發(fā)工具(如 latch,信號(hào)量 或 屏障)創(chuàng)建自己的同步機(jī)制。其它場(chǎng)景,您可以使用鎖或互斥鎖保護(hù)代碼的多線程訪問(wèn)。

          Kotlin 中的 Mutex 具有 lock 和 unlock 的掛起函數(shù)以用來(lái)手動(dòng)保護(hù)協(xié)程代碼。Mutex.withLock 擴(kuò)展函數(shù)使用很簡(jiǎn)單:

          class?TransactionsRepository(
          ??private?val?defaultDispatcher:?CoroutineDispatcher?=?Dispatchers.Default
          )?{

          ??//?Mutex?保護(hù)緩存可變狀態(tài)
          ??private?val?cacheMutex?=?Mutex()
          ??private?val?transactionsCache?=?mutableMapOf()

          ??private?suspend?fun?addTransaction(user:?User,?transaction:?Transaction)?=
          ????withContext(defaultDispatcher)?{
          ??????//?Mutex?使?讀&寫?緩存的操作?線程安全
          ??????cacheMutex.withLock?{
          ????????if?(transactionsCache.contains(user))?{
          ??????????val?oldList?=?transactionsCache[user]
          ??????????val?newList?=?oldList!!.toMutableList()
          ??????????newList.add(transaction)
          ??????????transactionsCache.put(user,?newList)
          ????????}?else?{
          ??????????transactionsCache.put(user,?listOf(transaction))
          ????????}
          ??????}
          ????}
          }

          由于使用 Mutex 的協(xié)程在可以繼續(xù)執(zhí)行前會(huì)暫停執(zhí)行,因此它比阻塞線程的 JVM 鎖要有效得多。在協(xié)程中使用 JVM 同步類時(shí)要小心,因?yàn)檫@可能會(huì)阻塞在其中執(zhí)行協(xié)程的線程并產(chǎn)生 liveness 問(wèn)題。


          傳遞給協(xié)程構(gòu)建器的代碼塊最終在一個(gè)或多個(gè) JVM 線程上執(zhí)行。因此,協(xié)程運(yùn)行在 JVM 線程模型中并受其所有約束。使用協(xié)程,仍會(huì)寫出錯(cuò)誤的多線程代碼。因此,在代碼中訪問(wèn)共享的可變狀態(tài)要小心!

          譯文完。

          譯者總結(jié)

          • 基于 JVM 的 Kotlin 協(xié)程本質(zhì)上是基于 JVM 線程池工作的
          • 協(xié)程是 Android 中異步編程的推薦方案
          • 協(xié)程也存在并發(fā)問(wèn)題,開發(fā)者需要注意并解決
          • 并發(fā)問(wèn)題的根源在于狀態(tài)管理
          • 保護(hù)可變狀態(tài)需要視具體情況而定,但有一些小技巧

          推薦閱讀

          • 并發(fā)問(wèn)題出現(xiàn)的源頭

          • 線程池

          • 碼上開學(xué):Kotlin 的協(xié)程用力瞥一眼 - 學(xué)不會(huì)協(xié)程?很可能因?yàn)槟憧催^(guò)的教程都是錯(cuò)的

          • 碼上開學(xué):Kotlin 協(xié)程的掛起好神奇好難懂?今天我把它的皮給扒了

          • 碼上開學(xué):到底什么是「非阻塞式」掛起?協(xié)程真的更輕量級(jí)嗎

          關(guān)于我

          人總是喜歡做能夠獲得正反饋(成就感)的事情,如果感覺本文內(nèi)容對(duì)你有幫助的話,麻煩點(diǎn)亮一下??,這對(duì)我很重要哦~

          我是 Flywith24,「人只有通過(guò)和別人的討論,才能知道我們自己的經(jīng)驗(yàn)是否是真實(shí)的」,加我微信交流,讓我們共同進(jìn)步。

          微信:Flywith24


          瀏覽 42
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  夜夜躁日日躁狠狠躁av麻豆 | 艹艹视频 | 国产精品精品 | 亚洲五月花 | 国产福利一区二区在线观看 |