Kotlin 協程

為什么要搞出和用協程呢

是節省CPU,避免系統內核級的線程頻繁切換,造成的CPU資源浪費。好鋼用在刀刃上。而協程是用戶態的線程,用戶可以自行控制協程的創建于銷毀,極大程度避免了系統級線程上下文切換造成的資源浪費。

是節約內存,在64位的Linux中,一個線程需要分配8MB棧內存和64MB堆內存,系統內存的制約導致我們無法開啟更多線程實現高并發。而在協程編程模式下,可以輕松有十幾萬協程,這是線程無法比擬的。

是穩定性,前面提到線程之間通過內存來共享數據,這也導致了一個問題,任何一個線程出錯時,進程中的所有線程都會跟著一起崩潰。

是開發效率,使用協程在開發程序之中,可以很方便的將一些耗時的IO操作異步化,例如寫文件、耗時IO請求等。

對于協程的一個總結

特征:協程是運行在單線程中的并發程序

優點:省去了傳統 Thread 多線程并發機制中切換線程時帶來的線程上下文切換、線程狀態切換、Thread 初始化上的性能損耗,能大幅度的提高并發性能

簡單理解:在單線程上由程序員自己調度運行的并行計算

寫到最后

協程本身不是替換線程的,因為協程是建立在線程之上的,但是協程能夠更好的為我們提供執行高并發任務

1.kotlin中協程的特點:可以用同步的方式寫出異步的代碼

coroutineScope.launch(Dispatchers.Main){// 開始協程:主線程

? ? val token=api.getToken()// 網絡請求:IO 線程

? ? val user=api.getUser(token)// 網絡請求:IO 線程

? ? nameTv.text=user.name// 更新 UI:主線程

}

2.協程中掛起的本質

啟動一個協程可以使用 launch 或者 async 函數,協程其實就是這兩個函數中閉包的代碼塊。

launch ,async 或者其他函數創建的協程,在執行到某一個 suspend 函數的時候,這個協程會被「suspend」,也就是被掛起。

3.協程的代碼塊中,線程執行到了 suspend 函數這里的時候,就暫時不再執行剩余的協程代碼,跳出協程的代碼塊。

那線程接下來會做什么呢?

如果它是一個后臺線程:

要么無事可做,被系統回收

要么繼續執行別的后臺任務

跟 Java 線程池里的線程在工作結束之后是完全一樣的:回收或者再利用。

如果這個線程它是 Android 的主線程,那它接下來就會繼續回去工作:也就是一秒鐘 60 次的界面刷新任務。

一個常見的場景是,獲取一個圖片,然后顯示出來:

// 主線程中

GlobalScope.launch(Dispatchers.Main){

? ? valimage=suspendingGetImage(imageId)// 獲取圖片

? ? avatarIv.setImageBitmap(image)// 顯示出來

}

suspend fun suspendingGetImage(id:String)=withContext(Dispatchers.IO){...}

協程:

線程的代碼在到達suspend函數的時候被掐斷,接下來協程會從這個suspend函數開始繼續往下執行,不過是在指定的線程

誰指定的?是suspend函數指定的,比如我們這個例子中,函數內部的withContext傳入的Dispatchers.IO所指定的 IO 線程。

Dispatchers調度器,它可以將協程限制在一個特定的線程執行,或者將它分派到一個線程池,或者讓它不受限制地運行,關于Dispatchers這里先不展開了。

那我們平日里常用到的調度器有哪些?

常用的Dispatchers,有以下三種:

Dispatchers.Main:Android 中的主線程

Dispatchers.IO:針對磁盤和網絡 IO 進行了優化,適合 IO 密集型的任務,比如:讀寫文件,操作數據庫以及網絡請求

Dispatchers.Default:適合 CPU 密集型的任務,比如計算

回到我們的協程,它從suspend函數開始脫離啟動它的線程,繼續執行在Dispatchers所指定的 IO 線程。

緊接著在suspend函數執行完成之后,協程為我們做的最爽的事就來了:會自動幫我們把線程再切回來

這個「切回來」是什么意思?

我們的協程原本是運行在主線程的,當代碼遇到 suspend 函數的時候,發生線程切換,根據Dispatchers切換到了 IO 線程;

當這個函數執行完畢后,線程又切了回來,「切回來」也就是協程會幫我再post一個Runnable,讓我剩下的代碼繼續回到主線程去執行。

我們從線程和協程的兩個角度都分析完成后,終于可以對協程的「掛起」suspend 做一個解釋:

協程在執行到有 suspend 標記的函數的時候,會被 suspend 也就是被掛起,而所謂的被掛起,就是切個線程;

不過區別在于,掛起函數在執行完成之后,協程會重新切回它原先的線程

再簡單來講,在 Kotlin 中所謂的掛起,就是一個稍后會被自動切回來的線程調度操作

這個「切回來」的動作,在 Kotlin 里叫做 resume,恢復。

通過剛才的分析我們知道:掛起之后是需要恢復。

而恢復這個功能是協程的,如果你不在協程里面調用,恢復這個功能沒法實現,所以也就回答了這個問題:為什么掛起函數必須在協程或者另一個掛起函數里被調用。

再細想下這個邏輯:一個掛起函數要么在協程里被調用,要么在另一個掛起函數里被調用,那么它其實直接或者間接地,總是會在一個協程里被調用的。

所以,要求suspend函數只能在協程里或者另一個 suspend 函數里被調用,還是為了要讓協程能夠在suspend函數切換線程之后再切回

通過剛才的分析我們知道:掛起之后是需要恢復。

而恢復這個功能是協程的,如果你不在協程里面調用,恢復這個功能沒法實現,所以也就回答了這個問題:為什么掛起函數必須在協程或者另一個掛起函數里被調用。

再細想下這個邏輯:一個掛起函數要么在協程里被調用,要么在另一個掛起函數里被調用,那么它其實直接或者間接地,總是會在一個協程里被調用的。

所以,要求suspend函數只能在協程里或者另一個 suspend 函數里被調用,還是為了要讓協程能夠在 suspend 函數切換線程之后再切回來。

什么是「非阻塞式掛起」

非阻塞式是相對阻塞式而言的。

編程語言中的很多概念其實都來源于生活,就像脫口秀的段子一樣。

線程阻塞很好理解,現實中的例子就是交通堵塞,它的核心有 3 點:

前面有障礙物,你過不去(線程卡了)

需要等障礙物清除后才能過去(耗時任務結束)

除非你繞道而行(切到別的線程)

從語義上理解「非阻塞式掛起」,講的是「非阻塞式」這個是掛起的一個特點,也就是說,協程的掛起,就是非阻塞式的,協程是不講「阻塞式的掛起」的概念的。

我們講「非阻塞式掛起」,其實它有幾個前提:并沒有限定在一個線程里說這件事,因為掛起這件事,本來就是涉及到多線程。

就像視頻里講的,阻塞不阻塞,都是針對單線程講的,一旦切了線程,肯定是非阻塞的,你都跑到別的線程了,之前的線程就自由了,可以繼續做別的事情了。

所以「非阻塞式掛起」,其實就是在講協程在掛起的同時切線程這件事情。

為什么要講非阻塞式掛起

「非阻塞式掛起」和第二篇的「掛起要切線程」是同一件事情,那還有講的必要嗎?

是有的。因為它在寫法上和單線程的阻塞式是一樣的。

協程只是在寫法上「看起來阻塞」,其實是「非阻塞」的,因為在協程里面它做了很多工作,其中有一個就是幫我們切線程。

之前說的掛起,重點是說切線程先切過去,然后再切回來。

而這里的非阻塞式,重點是說線程雖然會切,但寫法上和普通的單線程差不多。

讓我們來看看下面的例子:

main{

GlobalScope.launch(Dispatchers.Main){// 耗時操作val user=suspendingRequestUser()

updateView(user)

}

private suspend fun suspendingRequestUser():User=withContext(Dispatchers.IO){api.requestUser()}}

阻塞的本質

首先,所有的代碼本質上都是阻塞式的,而只有比較耗時的代碼才會導致人類可感知的等待,比如在主線程上做一個耗時 50 ms 的操作會導致界面卡掉幾幀,這種是我們人眼能觀察出來的,而這就是我們通常意義所說的「阻塞」。

舉個例子,當你開發的 app 在性能好的手機上很流暢,在性能差的老手機上會卡頓,就是在說同一行代碼執行的時間不一樣。

視頻中講了一個網絡 IO 的例子,IO 阻塞更多是反映在「等」這件事情上,它的性能瓶頸是和網絡的數據交換,你切多少個線程都沒用,該花的時間一點都少不了。

而這跟協程半毛錢關系沒有,切線程解決不了的事情,協程也解決不了

總結

關于這邊文章的標題協程是什么、掛起是什么、掛起的非阻塞式可以做下面的總結

協程是什么

協程就是切線程;

掛起是什么

掛起就是可以自動切回來的切線程;

掛起的非阻塞式

掛起的非阻塞式指的是它能用看起來阻塞的代碼寫出非阻塞的操作。

2.在kotlin使用協程

項目中配置對 Kotlin 協程的支持

在使用協程之前,我們需要在 build.gradle 文件中增加對 Kotlin 協程的依賴:

項目根目錄下的 build.gradle :

buildscript {

? ? ext.kotlin_coroutines = '1.4.0'

}

Module 下的 build.gradle

dependencies {

? ? ? ? implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0'

}

創建協程

kotlin 中 GlobalScope 類提供了幾個攜程構造函數:

launch - 創建協程

async - 創建帶返回值的協程,返回的是 Deferred 類

withContext - 不創建新的協程,指定協程上運行代碼塊

runBlocking - 不是 GlobalScope 的 API,可以獨立使用,區別是 runBlocking 里面的 delay 會阻塞線程,而 launch 創建的不會

先跑起來一個簡單的例子:

import kotlinx.coroutines.*

fun main(){

? ? ? ? GlobalScope.launch{

? ? ? ? // 在后臺啟動一個新的協程并繼續

? ? ? ? delay(1000L)// 非阻塞的等待 1 秒鐘(默認時間單是毫秒)

? ? ? ? println("World!")// 在延遲后打印輸出}

? ? ? ? println("Hello,")// 協程已在等待時主線程還在繼續

? ? ? ? Thread.sleep(2000L)// 阻塞主線程 2 秒鐘來保證 JVM 存活

}

協程中卻有一個很實用的函數:withContext 。這個函數可以切換到指定的線程,并在閉包內的邏輯執行結束之后,自動把線程切回去繼續執行。那么可以將上面的代碼寫成這樣:

coroutineScope.launch(

? ? Dispatchers.Main){//? 在 UI 線程開始

? ? val image=withContext(Dispatchers.IO){// 切換到 IO 線程,并在執行完成后切回 UI 線程

? ? getImage(imageId)// 將會運行在 IO 線程}

? ? avatarIv.setImageBitmap(image)// 回到 UI 線程更新 UI

}

我們甚至可以把 withContext 放進一個單獨的函數里面:

launch(Dispatchers.Main){//? 在 UI 線程開始

val image=getImage(imageId)

avatarIv.setImageBitmap(image)//? 執行結束后,自動切換回 UI 線程}//

suspend fun getImage(imageId:Int)=withContext(Dispatchers.IO){...}


launch 函數

launch 函數定義

public fun CoroutineScope.launch(

? ? context:CoroutineContext=EmptyCoroutineContext,

? ? start:CoroutineStart=CoroutineStart.DEFAULT,

? ? block:suspendCoroutineScope.()->Unit):Job

launch 是個擴展函數,接受3個參數,前面2個是常規參數,最后一個是個對象式函數,這樣的話 kotlin 就可以使用以前說的閉包的寫法:() 里面寫常規參數,{} 里面寫函數式對象的實現,就像上面的例子一樣

我們需要關心的是 launch 的3個參數和返回值 Job:

CoroutineContext - 可以理解為協程的上下文,在這里我們可以設置 CoroutineDispatcher 協程運行的線程調度器,有 4種線程模式:

Dispatchers.Default

Dispatchers.IO -

Dispatchers.Main - 主線程

Dispatchers.Unconfined - 沒指定,就是在當前線程

不寫的話就是 Dispatchers.Default 模式的,或者我們可以自己創建協程上下文,也就是線程池,newSingleThreadContext 單線程,newFixedThreadPoolContext 線程池

CoroutineStart - 啟動模式,默認是DEAFAULT,也就是創建就啟動;還有一個是LAZY,意思是等你需要它的時候,再調用啟動

DEAFAULT - 模式模式,不寫就是默認

ATOMIC -

UNDISPATCHED

LAZY - 懶加載模式,你需要它的時候,再調用啟動

block - 閉包方法體,定義協程內需要執行的操作

Job - 協程構建函數的返回值,可以把 Job 看成協程對象本身,協程的操作方法都在 Job 身上了

job.start() - 啟動協程,除了 lazy 模式,協程都不需要手動啟動

job.join() - 等待協程執行完畢

job.cancel() - 取消一個協程

job.cancelAndJoin() - 等待協程執行完畢然后再取消

創建一個協程

創建該launch函數返回了一個可以被用來取消運行中的協程的Job

val job=launch{

? ? repeat(1000){i->

? ? ? ? println("job: I'm sleeping$i...")

? ? ? ? delay(500L)

? ? }

}

取消

val? job=launch{

repeat(1000){

i->println("job: I'm sleeping $i ...")

delay(500L)}}

delay(1300L)// 延遲一段時間

println("main: I'm tired of waiting!")job.cancel()// 取消該作業

job.join()// 等待作業執行結束

println("main: Now I can quit.")

程序執行后的輸出如下:

job: I'm sleeping 0 ...

job: I'm sleeping 1 ...

job: I'm sleeping 2 ...

main: I'm tired of waiting!

main: Now I can quit.

一旦 main 函數調用了 job.cancel,我們在其它的協程中就看不到任何輸出,因為它被取消了

超時

在實踐中絕大多數取消一個協程的理由是它有可能超時。 當你手動追蹤一個相關 Job的引用并啟動了一個單獨的協程在延遲后取消追蹤,這里已經準備好使用 withTimeout 函數來做這件事。 來看看示例代碼:

withTimeout(1300L){repeat(1000){i->

println("I'm sleeping $i ...")

delay(500L)}}

運行后得到如下輸出:

I'm sleeping 0 ...

I'm sleeping 1 ...

I'm sleeping 2 ...

Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

這里我們看到了TimeoutCancellationException異常,這異常是因為超時導致的異常取消

解決這個問題也很簡單通過withTimeout的withTimeoutOrNull函數,代碼示例:

valresult=withTimeoutOrNull(1300L){repeat(1000){i->println("I'm sleeping$i...")delay(500L)}"Done"http:// 在它運行得到結果之前取消它}println("Result is$result")

運行后得到如下輸出:

I'm sleeping 0 ...

I'm sleeping 1 ...

I'm sleeping 2 ...

Result is null

這樣就沒有拋出異常了


async 函數定義

public fun <T> CoroutineScope.async(

? ? context:CoroutineContext=EmptyCoroutineContext,

? ? start:CoroutineStart=CoroutineStart.DEFAULT,

? ? block:suspendCoroutineScope.()->T):Deferred<T>{

? ? ? ? val? newContext=newCoroutineContext(context)

? ? ? ? val coroutine=if(start.isLazy){

? ? ? ? LazyDeferredCoroutine(newContext,block)

}else{DeferredCoroutine<T>(newContext,active=true)

? ? ? ? coroutine.start(start,coroutine,block)

? ? ? ? return coroutine

}

從源碼可以看出launch 和 async的唯一區別在于async的返回值

async 返回的是 Deferred 類型,Deferred 繼承自 Job 接口,Job有的它都有,增加了一個方法 await ,這個方法接收的是 async 閉包中返回的值,async 的特點是不會阻塞當前線程,但會阻塞所在協程,也就是掛起

但是需要注意的是async 并不會阻塞線程,只是阻塞鎖調用的協程

async和launch的區別

launch 更多是用來發起一個無需結果的耗時任務,這個工作不需要返回結果。

async 函數則是更進一步,用于異步執行耗時任務,并且需要返回值(如網絡請求、數據庫讀寫、文件讀寫),在執行完畢通過 await() 函數獲取返回值。

runBlocking

runBlocking啟動的協程任務會阻斷當前線程,直到該協程執行結束。當協程執行結束之后,頁面才會被顯示出來。

runBlocking 通常適用于單元測試的場景,而業務開發中不會用到這個函數

relay、yield

relay 和 yield 方法是協程內部的操作,可以掛起協程,

relay、yield 的區別

relay 是掛起協程并經過執行時間恢復協程,當線程空閑時就會運行協程

yield 是掛起協程,讓協程放棄本次 cpu 執行機會讓給別的協程,當線程空閑時再次運行協程。

我們只要使用 kotlin 提供的協程上下文類型,線程池是有多個線程的,再次執行的機會很快就會有的。

除了 main 類型,協程在掛起后都會封裝成任務放到協程默認線程池的任務隊列里去,有延遲時間的在時間過后會放到隊列里去,沒有延遲時間的直接放到隊列里去


原文鏈接:http://www.lxweimin.com/p/402a69dbd66d

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • [TOC] 簡介 Coroutines are computer program components that ...
    Whyn閱讀 6,007評論 5 15
  • 在今年的三月份,我因為需要為項目搭建一個新的網絡請求框架開始接觸 Kotlin 協程。那時我司項目中同時存在著兩種...
    Android開發指南閱讀 869評論 0 2
  • 在今年的三月份,我因為需要為項目搭建一個新的網絡請求框架開始接觸 Kotlin 協程。那時我司項目中同時存在著兩種...
    業志陳閱讀 1,105評論 0 5
  • 協程是輕量級的線程。 kotlin協程是kotlin的擴展庫(kotlinx.coroutines)。 線程在An...
    付小影子閱讀 6,459評論 0 4
  • 久違的晴天,家長會。 家長大會開好到教室時,離放學已經沒多少時間了。班主任說已經安排了三個家長分享經驗。 放學鈴聲...
    飄雪兒5閱讀 7,557評論 16 22