成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

深入淺出協程、線程和并發問題

開發 前端
本文的內容會告訴大家 協程是如何在 Android 運行時中被運行的 ,它們和線程之間的關系是什么,以及在使用 Java 編程語言線程模型時所遇到的 并發問題 。

 [[403493]]

"協程是輕量級的線程",相信大家不止一次聽到這種說法。但是您真的理解其中的含義嗎?恐怕答案是否定的。接下來的內容會告訴大家 協程是如何在 Android 運行時中被運行的 ,它們和線程之間的關系是什么,以及在使用 Java 編程語言線程模型時所遇到的 并發問題 。

協程和線程

協程旨在簡化異步執行的代碼。對于 Android 運行時的協程, lambda 表達式的代碼塊會在專門的線程中執行 。例如,示例中的斐波那契 運算:

  1. // 在后臺線程中運算第十級斐波那契數 
  2. someScope.launch(Dispatchers.Default) { 
  3.     val fibonacci10 = synchronousFibonacci(10
  4.     saveFibonacciInMemory(10, fibonacci10) 
  5.  
  6. private fun synchronousFibonacci(n: Long): Long { /* ... */ } 

上面 async 協程的代碼塊, 會被分發到由協程庫所管理的線程池中執行 ,實現了同步且阻塞的斐波那契數值運算,并且將結果存入內存,上例中的線程池屬于 Dispatchers.Default。該代碼塊會在未來某些時間在線程池中的某一線程中執行,具體執行時間取決于線程池的策略。

請注意由于上述代碼中未包含掛起操作,因此它會在同一個線程中執行。而協程是有可能在不同的線程中執行的,比如將執行部分移動到不同的分發器,或者在使用線程池的分發器中包含帶有掛起操作的代碼。

如果不使用協程的話,您還可以使用線程自行實現類似的邏輯,代碼如下:

  1. // 創建包含 4 個線程的線程池 
  2. val executorService = Executors.newFixedThreadPool(4
  3.   
  4. // 在其中的一個線程中安排并執行代碼 
  5. executorService.execute { 
  6.     val fibonacci10 = synchronousFibonacci(10
  7.     saveFibonacciInMemory(10, fibonacci10) 

雖然您可以自行實現線程池的管理, 但是我們仍然推薦使用協程作為 Android 開發中首選的異步實現方案 ,它具備內置的取消機制,可以提供更便捷的異常捕捉和結構式并發,后者可以減少類似內存泄漏問題的發生幾率,并且與 Jetpack 庫集成度更高。

工作原理

從您創建協程到代碼被線程執行這期間發生了什么呢?當您使用標準的協程 builder 創建協程時,您可以指定該協程所運行的 CoroutineDispatcher ,如果未指定,系統會默認使用 Dispatchers.Default 。

CoroutineDispatcher 會負責將協程的執行分配到具體的線程。在底層,當 CoroutineDispatcher 被調用時,它會調用 封裝了 Continuation (比如這里的協程) interceptContinuation 方法來攔截協程。該流程是以 CoroutineDispatcher 實現了 CoroutineInterceptor 接口作為前提。

如果您閱讀了我之前的關于協程在底層是如何實現 的文章,您應該已經知道了編譯器會創建狀態機,以及關于狀態機的相關信息 (比如接下來要執行的操作) 是被存儲在Continuation 對象中。

一旦 Continuation 對象需要在另外的 Dispatcher 中執行, DispatchedContinuation 的 resumeWith 方法會負責將協程分發到合適的 Dispatcher。

此外,在 Java 編程語言的實現中, 繼承自 DispatchedTask 抽象類的 DispatchedContinuation 也屬于 Runnable 接口的一種實現類型。因此, DispatchedContinuation 對象也可以在線程中執行。其中的好處是當指定了 CoroutineDispatcher 時,協程就會轉換為 DispatchedTask ,并且作為 Runnable 在線程中執行。

那么當您創建協程后, dispatch 方法如何被調用呢?當您使用標準的協程 builder 創建協程時,您可以指定啟動參數,它的類型是CoroutineStart。例如,您可以設置協程在需要的時候才啟動,這時可以將參數設置為 CoroutineStart.LAZY 。默認情況下,系統會使用 CoroutineStart.DEFAULT 根據 CoroutineDispatcher 來安排執行時機。

△ 協程的代碼塊如何在線程中執行的示意圖

分發器和線程池

您可以使用 Executor.asCoroutineDispatcher() 擴展函數將協程轉換為 CoroutineDispatcher 后,即可在應用中的任何線程池中執行該協程。此外,您還可以使用協程庫默認的 Dispatchers 。

您可以看到 createDefaultDispatcher 方法中是如何初始化 Dispatchers.Default 的。默認情況下,系統會使用 DefaultScheduler 。如果您看一下 Dispatcher.IO 的實現代碼,它也使用了 DefaultScheduler ,支持按需創建至少 64 個線程。 Dispatchers.Default 和 Dispatchers.IO 是隱式關聯的,因為它們使用了同一個線程池,這就引出了我們下一個話題,使用不同的分發器調用 withContext 會帶來哪些運行時的開銷呢?

線程和 withContext 的性能表現

在 Android 運行時中,如果運行的線程比 CPU 的可用內核數多,那么切換線程會帶來一定的運行時開銷。 上下文切換 并不輕松!操作系統需要保存和恢復執行的上下文,而且 CPU 除了執行實際的應用功能之外,還需要花時間規劃線程。除此之外,當線程中所運行代碼阻塞的時候也會造成上下文切換。如果上述的問題是針對線程的,那么在不同的 Dispatchers 中使用 withContext 會帶來哪些性能上的損失呢?

還好線程池會幫我們解決這些復雜的操作,它會嘗試盡量多地執行任務 (這也是為什么在線程池中執行操作要優于手動創建線程)。協程由于被安排在線程池中執行,所以也會從中受益?;诖耍瑓f程不會阻塞線程,它們反而會掛起自己的工作,因而更加有效。

Java 編程語言中默認使用的線程池是 CoroutineScheduler 。 它以最高效的方式將協程分發到工作線程 。由于 Dispatchers.Default 和 Dispatchers.IO 使用相同的線程池,在它們之間切換會盡量避免線程切換。協程庫會優化這些切換調用,保持在同一個分發器和線程上,并且盡量走捷徑。

由于 Dispatchers.Main 在帶有 UI 的應用中通常屬于不同的線程,所以協程中 Dispatchers.Default和 Dispatchers.Main 之間的切換并不會帶來太大的性能損失,因為協程會掛起 (比如在某個線程中停止執行),然后會被安排在另外的線程中繼續執行。

協程中的并發問題

協程由于其能夠簡單地在不同線程上規劃操作,的確使得異步編程更加輕松。但是另一方面,便捷是一把雙刃劍: 由于協程是運行在 Java 編程語言的線程模型之上,它們難以逃脫線程模型所帶來的并發問題 。因此,您需要注意并且盡量避免該問題。

近年來,像不可變性這樣的策略相對減輕了由線程所引發的問題。然而,有些場景下,不可變性策略也無法完全避免問題的出現。所有并發問題的源頭都是狀態管理!尤其是在一個多線程環境下訪問 可變的狀態 。

在多線程應用中,操作的執行順序是不可預測的。與編譯器優化操作執行順序不同,線程無法保證以特定的順序執行,而上下文切換會隨時發生。如果在訪問可變狀態時沒有采取必要的防范措施,線程就會訪問到過時的數據,丟失更新,或者遇到資源競爭 問題等等。

請注意這里所討論的可變狀態和訪問順序并不僅限于 Java 編程語言。它們在其它平臺上同樣會影響協程執行。

使用了協程的應用本質上就是多線程應用。 使用了協程并且涉及可變狀態的類必須采取措施使其可控 ,比如保證協程中的代碼所訪問的數據是最新的。這樣一來,不同的線程之間就不會互相干擾。并發問題會引起潛在的 bug,使您很難在應用中調試和定位問題,甚至出現海森堡 bug。

這一類型的類非常常見。比如該類需要將用戶的登錄信息緩存在內存中,或者當應用在活躍狀態時緩存一些值。如果您稍有大意,那么并發問題就會乘虛而入!使用 withContext(defaultDispatcher) 的掛起函數無法保證會在同一個線程中執行。

比如我們有一個類需要緩存用戶所做的交易。如果緩存沒有被正確訪問,比如下面代碼所示,就會出現并發問題:

  1. class TransactionsRepository( 
  2.   private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default 
  3. ) { 
  4.  
  5.   private val transactionsCache = mutableMapOf<User, List<Transaction>() 
  6.  
  7.   private suspend fun addTransaction(user: User, transaction: Transaction) = 
  8.     // 注意!訪問緩存的操作未被保護! 
  9.     // 會出現并發問題:線程會訪問到過期數據 
  10.     // 并且出現資源競爭問題 
  11.     withContext(defaultDispatcher) { 
  12.       if (transactionsCache.contains(user)) { 
  13.         val oldList = transactionsCache[user] 
  14.         val newList = oldList!!.toMutableList() 
  15.         newList.add(transaction) 
  16.         transactionsCache.put(user, newList) 
  17.       } else { 
  18.         transactionsCache.put(user, listOf(transaction)) 
  19.       } 
  20.     } 

即使我們這里所討論的是 Kotlin,由 Brian Goetz 所編撰的《Java 并發編程實踐》對于了解本文主題和 Java 編程語言系統是非常好的參考材料。此外,Jetbrains 針對共享可變的狀態和并發 的主題也提供了相關的文檔。

保護可變狀態

對于如何保護可變狀態,或者找到合適的同步 策略,取決于數據本身和相關的操作。本節內容啟發大家注意可能會遇到的并發問題,而不是簡單羅列保護可變狀態的方法和 API??偠灾?,這里為大家準備了一些提示和 API 可以幫助大家針對可變變量實現線程安全。

封裝

可變狀態應該屬于并被封裝在類里。該類應該將狀態的訪問操作集中起來,根據應用場景使用同步策略保護變量的訪問和修改操作。

線程限制

一種方案是將讀取和寫入操作限制在一個線程里??梢允褂藐犃谢谏a者-消費者模式實現對可變狀態的訪問。Jetbrains 對此提供了很棒的文檔。

避免重復工作

在 Android 運行時中,包含線程安全的數據結構可供您保護可變變量。比如,在計數器示例中,您可以使用AtomicInteger。又比如,要保護上述代碼中的 Map,您可以使用ConcurrentHashMap。 ConcurrentHashMap 是線程安全的,并且優化了 map 的讀取和寫入操作的吞吐量。

請注意,線程安全的數據結構并不能解決調用順序問題,它們只是確保內存數據的訪問是原子操作。當邏輯不太復雜的時候,它們可以避免使用 lock。比如,它們無法用在上面的 transactionCache 示例中,因為它們之間的操作順序和邏輯需要使用線程并進行訪問保護。

而且,當已修改的對象已經存儲在這些線程安全的數據結構中時,其中的數據需要保持不可變或者受保護狀態來避免資源競爭問題。

自定義方案

如果您有復合的操作需要被同步,@Volatile 和線程安全的數據結構也不會有效果。有可能內置的@Synchronized 注解的粒度也不足以達到理想效果。

在這些情況下,您可能需要使用并發工具創建您自己的同步機制,比如latches、semaphores 或者barriers。其它場景下,您可以使用lock 和 mutex 無條件地保護多線程訪問。

Kotlin 中的Mute 包含掛起函數lock 和unlock,可以手動控制保護協程的代碼。而擴展函數Mutex.withLock 使其更加易用:

  1. class TransactionsRepository( 
  2.   private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default 
  3. ) { 
  4.   // Mutex 保護可變狀態的緩存 
  5.   private val cacheMutex = Mutex() 
  6.   private val transactionsCache = mutableMapOf<User, List<Transaction>() 
  7.  
  8.   private suspend fun addTransaction(user: User, transaction: Transaction) = 
  9.     withContext(defaultDispatcher) { 
  10.       // Mutex 保障了讀寫緩存的線程安全 
  11.       cacheMutex.withLock { 
  12.         if (transactionsCache.contains(user)) { 
  13.           val oldList = transactionsCache[user] 
  14.           val newList = oldList!!.toMutableList() 
  15.           newList.add(transaction) 
  16.           transactionsCache.put(user, newList) 
  17.         } else { 
  18.           transactionsCache.put(user, listOf(transaction)) 
  19.         } 
  20.       } 
  21.     } 

由于使用 Mutex 的協程在可以繼續執行之前會掛起操作,因此要比 Java 編程語言中的 lock 高效很多,因為后者會阻塞整個線程。在協程中請謹慎使用 Java 語言中的同步類,因為它們會阻塞整個協程所處的線程,并且引發活躍度 問題。

傳入協程中的代碼最終會在一個或者多個線程中執行。同樣的,協程在 Android 運行時的線程模型下依然需要遵循約束條件。所以,使用協程也同樣會出現存在隱患的多線程代碼。所以,在代碼中請謹慎訪問共享的可變狀態。

 

責任編輯:張燕妮 來源: 開源中國博客
相關推薦

2021-06-04 14:28:07

協程線程Android開發

2021-03-16 08:54:35

AQSAbstractQueJava

2023-09-01 08:27:34

Java多線程程序

2011-07-04 10:39:57

Web

2019-01-16 17:05:02

Python亂碼網絡

2019-01-07 15:29:07

HadoopYarn架構調度器

2017-07-02 18:04:53

塊加密算法AES算法

2012-05-21 10:06:26

FrameworkCocoa

2021-07-20 15:20:02

FlatBuffers阿里云Java

2021-08-11 07:54:47

Commonjs

2022-09-26 09:01:15

語言數據JavaScript

2009-06-29 15:25:00

Java多線程

2009-11-30 16:46:29

學習Linux

2012-02-21 13:55:45

JavaScript

2022-01-11 07:52:22

CSS 技巧代碼重構

2018-11-09 16:24:25

物聯網云計算云系統

2019-11-11 14:51:19

Java數據結構Properties

2022-11-09 08:06:15

GreatSQLMGR模式

2022-12-02 09:13:28

SeataAT模式

2022-10-31 09:00:24

Promise數組參數
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 色爱区综合 | 免费观看一级特黄欧美大片 | 男人天堂视频在线观看 | 欧美日韩a | 国产大片黄色 | 天天天久久久 | 久久久久国产精品 | 国产日韩av一区二区 | 99久久久久| 欧美日韩在线视频一区 | 中文字幕一区在线观看视频 | 免费看的av | 国产91综合 | 精品欧美 | 黄色毛片免费视频 | 成人免费视频一区 | 久久精品国产精品青草 | 99久久婷婷国产综合精品电影 | 国产日韩欧美一区二区在线播放 | 成人一区二区视频 | 欧美中文字幕在线观看 | 国产精品免费在线 | 国产成人在线看 | 一级免费毛片 | 中文字幕第二区 | 亚洲成人一区二区在线 | 国产精品国产三级国产播12软件 | 欧美日韩一区二区在线观看 | 91精品在线播放 | 黄a免费网络 | 黄色一级大片在线免费看产 | 国产一级在线 | 亚洲狠狠| 精品永久 | 国产欧美一区二区在线观看 | 久久夜色精品国产 | 一区在线播放 | 在线毛片网 | 手机看片1 | 一区二区三区影院 | 国产电影精品久久 |