一.組合掛起函數
1.默認順序調用
假設我們在不同的地方定義了兩個進行某種調用遠程服務或者進行計算的掛起函數。我們只假設它們都是有用 的绍豁,但是實際上它們在這個示例中只是為了該目的而延遲了一秒鐘芯咧。
如果需要按 順序 調用它們,我們接下來會做什么——首先調用 doSomethingUsefulOne 接下來 調用 doSomethingUsefulTwo 竹揍,并且計算它們結果的和敬飒,我們使用普通的順序來進行調用,因為這些代碼是運行在協程中的芬位,只要像常規(guī)的代碼一樣 順序 都是默認的无拗。 下面的示例展示了測量執(zhí)行兩個掛起函數所需要的總時間:
運行結果:
2.使用 async 并發(fā)
如果 doSomethingUsefulOne 與 doSomethingUsefulTwo 之間沒有依賴,并且我們想更快的得到結 果昧碉,讓它們進行 并發(fā) 嗎?這就是 async 可以幫助我們的地方蓝纲。
在概念上,async 就類似于 launch晌纫。它啟動了一個單獨的協程税迷,這是一個輕量級的線程并與其它所有的協程一起 并發(fā)的工作。不同之處在于 launch 返回一個 Job 并且不附帶任何結果值锹漱,而 async 返回一個 Deferred —— 一個輕量級的非阻塞 future箭养,這代表了一個將會在稍后提供結果的 promise。你可以使用 .await() 在 一個延期的值上得到它的最終結果哥牍,但是 Deferred 也是一個 Job毕泌,所以如果需要的話喝检,你可以取消它。
運行結果:
這里快了兩倍撼泛,因為兩個協程并發(fā)執(zhí)行挠说。請注意,使用協程進行并發(fā)總是顯式的愿题。
3.惰性啟動的 async
async 可以通過將 start 參數設置為 CoroutineStart.LAZY 而變?yōu)槎栊缘乃鸺蟆T谶@個模式下,只有結果通過 await 獲取的時候協程才會啟動潘酗,或者在 Job 的 start 函數調用的時候杆兵。運行下面的示例:
運行結果:
因此,在先前的例子中這里定義的兩個協程沒有執(zhí)行仔夺,控制權在于程序員準確的在開始執(zhí)行時調用 start琐脏。 我們首先調用 one,然后調用 two缸兔,接下來等待這個協程執(zhí)行完畢日裙。
注意,如果我們只是在 println 中調用 await惰蜜,而沒有在單獨的協程中調用 start阅签,這將會導致順序行為,直到 await 啟動該協程 執(zhí)行并等待至它結束蝎抽,這并不是惰性的預期用例政钟。在計算一個值涉及掛起函數時,這個
async(start = CoroutineStart.LAZY) 的用例用于替代標準庫中的 lazy 函數(什么時候使用什么時候啟動)樟结。
4.async ?格的函數
我們可以定義異步?格的函數來 異步 的調用 doSomethingUsefulOne 和 doSomethingUsefulTwo 并 使用 async 協程建造器并帶有一個顯式的 GlobalScope 引用养交。我們給這樣的函數的名稱中加上“......Async”后 綴來突出表明:事實上,它們只做異步計算并且需要使用延期的值來獲得結果瓢宦。
注意碎连,這些 xxxAsync 函數不是 掛起 函數。它們可以在任何地方使用驮履。然而鱼辙,它們總是在調用它們的代碼中意 味著異步(這里的意思是 并發(fā) )執(zhí)行。
這種帶有異步函數的編程?格僅供參考玫镐,因為這在其它編程語言中是一種受歡迎的?格倒戏。在 Kotlin 的協程 中使用這種?格是強烈不推薦的,原因如下所述恐似。
考慮一下如果 val one = somethingUsefulOneAsync() 這一行和 one.await() 表達式這里在代碼 中有邏輯錯誤杜跷,并且程序拋出了異常,以及程序在操作的過程中中止,將會發(fā)生什么葛闷。通常情況下憋槐,一個全局的異 常處理者會捕獲這個異常,將異常打印成日記并報告給開發(fā)者淑趾,但是反之該程序將會繼續(xù)執(zhí)行其它操作阳仔。但是這里我們的 somethingUsefulOneAsync 仍然在后臺執(zhí)行,盡管如此扣泊,啟動它的那次操作也會被終止近范。這個程序將不會進行結構化并發(fā)。
5.使用 async 的結構化并發(fā)
讓我們使用使用 async 的并發(fā)這一小節(jié)的例子并且提取出一個函數并發(fā)的調用 doSomethingUsefulOne 與 doSomethingUsefulTwo 并且返回它們兩個的結果之和旷赖。由于 async 被定義為了 CoroutineScope 上 的擴展顺又,我們需要將它寫在作用域內更卒,并且這是 coroutineScope 函數所提供的:
這種情況下等孵,如果在 concurrentSum 函數內部發(fā)生了錯誤,并且它拋出了一個異常蹂空,所有在作用域中啟動的 協程都會被取消俯萌。
從上面的 main 函數的輸出可以看出,我們仍然可以同時執(zhí)行這兩個操作:
取消始終通過協程的層次結構來進行傳遞:
請注意上枕,如果其中一個子協程(即 two)失敗咐熙,第一個 async 以及等待中的父協程都會被取消:
所以,這種帶有異步函數的編程?格僅供參考辨萍,因為這在其它編程語言中是一種受歡迎的?格棋恼。在 Kotlin 的協程 中使用這種?格是強烈不推薦的
二.協程上下文與調度器
1.調度器與線程
協程總是運行在一些以 CoroutineContext 類型為代表的上下文中,它們被定義在了 Kotlin 的標準庫里锈玉。 協程上下文是各種不同元素的集合爪飘。其中主元素是協程中的 Job,我們在前面的介紹中?過它以及它的調度器拉背,
而下面將對它進行介紹师崎。
2.調度器與線程
協程上下文包含一個 協程調度器(CoroutineDispatcher),它確定了相關的協程在哪個線程或哪些線程上 執(zhí)行椅棺。協程調度器可以將協程限制在一個特定的線程執(zhí)行犁罩,或將它分派到一個線程池,亦或是讓它不受限地運行两疚。
所有的協程構建器諸如 launch 和 async 接收一個可選的 CoroutineContext 參數床估,它可以被用來顯式的為一 個新協程或其它上下文元素指定一個調度器。
運行結果:
當調用 launch { ...... } 時不傳參數诱渤,它從啟動了它的 CoroutineScope 中承襲了上下文(以及調度器)顷窒。在這 個案例中,它從 main 線程中的 runBlocking 主協程承襲了上下文。
Dispatchers.Unconfined 是一個特殊的調度器且似乎也運行在 main 線程中鞋吉,但實際上鸦做,它是一種不同的機制,開始運行可能會在main線程中谓着,如果掛起再次運行可能就會在其他線程中了泼诱。
當協程在 GlobalScope 中啟動時,使用的是由 Dispatchers.Default 代表的默認調度器赊锚。默認調度器使用共享 的后臺線程池治筒。所以launch(Dispatchers.Default) { ...... } 與 GlobalScope.launch { ...... } 使用相同的調度器。
newSingleThreadContext 為協程的運行啟動了一個線程舷蒲。一個專用的線程是一種非常昂貴的資源耸袜。在真實的 應用程序中兩者都必須被釋放,當不再需要的時候,使用 close 函數,或存儲在一個頂層變量中使它在整個應用 程序中被重用龙考。
3.非受限調度器 vs 受限調度器
Dispatchers.Unconfined 協程調度器在調用它的線程啟動了一個協程稠诲,但它僅僅只是運行到第一個掛起點。協程掛起后,非受限的協程調度器恢復線程中的協程,而這完全由被調用的掛起函數來決定(其實就是非受限調度器在調用他的線程中啟動了一個協程,當運行到第一個掛起點并掛起結束時沟使,再次運行協程就不在之前的線程中了)。非受限的調度器非常適用于執(zhí)行不消耗 CPU 時間的任務渊跋,以及不更新局限于特定線程的任何共享數據(如UI)的協程腊嗡。
另一方面,該調度器默認繼承了外部的 CoroutineScope拾酝。runBlocking 協程的默認調度器燕少,特別是,當它被限 制在了調用者線程時微宝,繼承自它將會有效地限制協程在該線程運行并且具有可預測的 FIFO 調度(先進先出棺亭,其實就是按順序從上到下執(zhí)行runBlocking中的協程)。
運行結果:
所以蟋软,該協程的上下文繼承自 runBlocking {...} 協程并在 main 線程中運行镶摘,當 delay 函數調用的時 候,非受限的那個協程在默認的執(zhí)行者線程中恢復執(zhí)行岳守。
非受限的調度器是一種高級機制凄敢,可以在某些極端情況下提供幫助而不需要調度協程以便稍后執(zhí)行或產生 不希望的副作用,因為某些操作必須立即在協程中執(zhí)行湿痢。非受限調度器不應該在通常的代碼中使用涝缝。
4.調試協程與線程
協程可以在一個線程上掛起并在其它線程上恢復扑庞。如果沒有特殊工具,甚至對于一個單線程的調度器也是難 以弄清楚協程在何時何地正在做什么事情拒逮。
用日志調試
讓線程在每一個日志文件的日志聲明中打印線程的名 字罐氨。這種特性在日志框架中是普遍受支持的。但是在使用協程時滩援,單獨的線程名稱不會給出很多協程上下文信 息栅隐,所以 kotlinx.coroutines 包含了調試工具來讓它更簡單。
運行結果:
使用 -Dkotlinx.coroutines.debug JVM 參數運行上面的代碼:
然后debug模式運行玩徊,結果如下:
可以看到除了線程的名字租悄,后邊還打印出了協程的名字
5.在不同線程間跳轉
其中一個使用 runBlocking 來顯式指定了一個上下文,并且另一個使用 withContext 函數來改變協程的上下文恩袱,而仍然駐留在相同的協程中泣棋。
運行結果:
其實就是同一個協程在不同的線程中跳轉
注意,在這個例子中畔塔,當我們不再需要某個在 newSingleThreadContext 中創(chuàng)建的線程的時候潭辈,它使用了 Kotlin 標準庫中的 use 函數來釋放該線程。
6.上下文中的作業(yè)
協程的 Job 是上下文的一部分俩檬,并且可以使用 coroutineContext [Job] 表達式在上下文中檢索它:
運行結果:
請注意萎胰,CoroutineScope 中的 isActive 只是 coroutineContext[Job]?.isActive == true 的一種方便的快捷方式碾盟。
意思就是CoroutineScope 中的 isActive的值就是coroutineContext[Job]?.isActive == true
7.子協程
當一個協程被其它協程在 CoroutineScope 中啟動的時候棚辽,它將通過 CoroutineScope.coroutineContext 來 承襲上下文,并且這個新協程的 Job 將會成為父協程作業(yè)的 子 作業(yè)冰肴。當一個父協程被取消的時候屈藐,所有它的子 協程也會被遞歸的取消。
然而熙尉,當使用 GlobalScope 來啟動一個協程時联逻,則新協程的作業(yè)沒有父作業(yè)。因此它與這個啟動的作用域無關 且獨立運作检痰。
運行結果:
啟動協程包归,
1,GlobalScope.launch的協程打印job1: I run in GlobalScope and execute independently!铅歼;
2公壤,100ms后,launch的協程打印job2: I am a child of the request coroutine椎椰;
3厦幅,500ms后,request協程被取消慨飘;
4确憨,1000ms后,GlobalScope.launch的協程打印job1: I am not affected by cancellation of the request;
5休弃,1000ms后吞歼,主協程打印main: Who has survived request cancellation?
從上邊的分析可以知道,當父協程(request)被取消后塔猾,子協程(launch)也被取消了(因為沒有打印job2: I will not execute this line if my parent request is cancelled)浆熔,而GlobalScope.launch協程正常打印,說明它與啟動的作用域無關桥帆,獨立運作医增。
8.父協程的職責
一個父協程總是等待所有的子協程執(zhí)行結束。父協程并不顯式的跟蹤所有子協程的啟動老虫,并且不必使用 Job.join 在最后的時候等待它們:
運行結果:
將上邊代碼中的request.join()注釋掉叶骨,再來執(zhí)行
運行結果:
可以看到,request.join()會等待request協程中所有子協程執(zhí)行完畢祈匙,主協程在繼續(xù)執(zhí)行忽刽。
無論主協程是否request.join(),request協程都會將子協程執(zhí)行完夺欲。
9.命名協程以用于調試
當協程經常打印日志并且你只需要關聯來自同一個協程的日志記錄時跪帝,則自動分配的 id 是非常好的。然而些阅,當 一個協程與特定請求的處理相關聯時或做一些特定的后臺任務伞剑,最好將其明確命名以用于調試目的。 CoroutineName 上下文元素與線程名具有相同的目的市埋。當調試模式開啟時黎泣,它被包含在正在執(zhí)行此協程的線程 名中。
程序執(zhí)行使用了 -Dkotlinx.coroutines.debug JVM 參數缤谎,運行結果:
10.組合上下文中的元素
有時我們需要在協程上下文中定義多個元素抒倚。我們可以使用 + 操作符來實現。比如說坷澡,我們可以顯式指定一個 調度器來啟動協程并且同時顯式指定一個命名:
運行結果:
11.協程作用域
讓我們將關于上下文托呕,子協程以及作業(yè)的知識綜合在一起。假設我們的應用程序擁有一個具有生命周期的對象频敛, 但這個對象并不是一個協程项郊。舉例來說,我們編寫了一個 Android 應用程序并在 Android 的 activity 上下文中 啟動了一組協程來使用異步操作拉取并更新數據以及執(zhí)行動畫等等姻政。所有這些協程必須在這個 activity 銷毀的 時候取消以避免內存泄漏呆抑。當然,我們也可以手動操作上下文與作業(yè)汁展,以結合 activity 的生命周期與它的協程鹊碍,但 是 kotlinx.coroutines 提供了一個封裝:CoroutineScope 的抽象厌殉。你應該已經熟悉了協程作用域,因為 所有的協程構建器都聲明為在它之上的擴展侈咕。
我們通過創(chuàng)建一個 CoroutineScope 實例來管理協程的生命周期公罕,并使它與 activity 的生命周期相關 聯。CoroutineScope 可以通過 CoroutineScope() 創(chuàng)建或者通過MainScope() 工廠函數創(chuàng)建(使用MainScope需要添加依賴:implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.4')耀销。前者創(chuàng)建了一個通 用作用域楼眷,而后者為使用 Dispatchers.Main 作為默認調度器的 UI 應用程序 創(chuàng)建作用域:
現在,我們可以使用定義的 scope 在這個 Activity 的作用域內啟動協程熊尉。對于該示例罐柳,我們啟動了十個協 程,它們會延遲不同的時間:
在 main 函數中我們創(chuàng)建 activity狰住,調用測試函數 doSomething 张吉,并且在 500 毫秒后銷毀這個 activity。這取 消了從 doSomething 啟動的所有協程催植。我們可以觀察到這些是由于在銷毀之后肮蛹,即使我們再等一會 兒,activity 也不再打印消息创南。
12.線程局部數據
有時伦忠,能夠將一些線程局部數據傳遞到協程與協程之間是很方便的。然而稿辙,由于它們不受任何特定線程的約束昆码, 如果手動完成,可能會導致出現樣板代碼邓深。
ThreadLocal未桥,asContextElement 擴展函數在這里會充當救兵笔刹。它創(chuàng)建了額外的上下文元素芥备,且保留給定 ThreadLocal 的值,并在每次協程切換其上下文時恢復它舌菜。
在這個例子中我們使用 Dispatchers.Default 在后臺線程池中啟動了一個新的協程萌壳,所以它工作在線程池中的 不同線程中,但它仍然具有線程局部變量的值日月,我們指定使用 threadLocal.asContextElement(value = "launch")袱瓮,無論協程執(zhí)行在哪個線程中,通過threadLocal.get()獲取到的值都是“l(fā)aunch”爱咬。
運行結果: