什么是Coroutines(協程)轉

轉自: http://www.lxweimin.com/p/acbd9bf65063

一、什么是Coroutines(協程)

協程是很久之前就提出的一個概念,目前支持協程的語言包括 lua、C#、go等。也包括Android官方開發語言Kotlin。當然網上對此也有很多的爭議,很多說法認為Kotlin中的協程是個偽協程,沒有實現go語言的那種協程特性,而僅僅是對于java線程的一個包裝,本文也認同這種觀點,因為它并沒有脫離JVM來實現,所以仍然受java線程模型限制。這里只去談論Kotlin協程的用法和原理,暫時拋開對于協程概念的不同理解。

kotlinx.coroutines 是由 JetBrains開發的功能豐富的協程庫。它涵蓋很多啟用高級協程的原語,包括 launch、 async 等等。

coroutines通過掛起函數的概念完成協程任務調度,協程是輕量級線程,本質上是在線程上進行任務調度。甚至可以粗俗的理解為類似于進程和線程的關系,一個進程中可以包括多個線程,而一個線程中可以包括多個協程。但執行上是有區別的,一個進程中可以有多個線程同時并發執行,但是一個線程中的多個協程本質上是順序執行的,是應用協程掛起的方式來表現為并發執行。

二、協程創建

1.協程的創建主要有三種方式:

1)launch創建。

返回值是Job,Job用來處理協程的取消等操作。這種創建方式是非阻塞的,創建的協程并不會阻塞創建協程的線程,也可以通過Job的join方法阻塞線程,來等待協程執行結束。如果當前創建處沒有協程上下文信息需要使用GlobalScope調用launch方法以頂層協程的方式創建。但是用GlobalScope.launch和直接用launch方式創建有一些區別,GlobalScope.launch默認是開啟新線程來執行協程任務的,launch是直接在當前上下文中的線程執行。

valcoroutineJob=GlobalScope.launch{Log.d(TAG,"current Thread is${Thread.currentThread()}")}Log.d(TAG,"GlobalScope.launch create coroutine")

可以看到輸出的日志順序是先輸出協程外部的日志,后輸出協程內部的日志,并且協程內部任務的執行是在工作線程。

2020-05-2115:52:39.13720964-20964/com.common.coroutines_retrofit_okhttp D/MainActivity:GlobalScope.launchcreate coroutine2020-05-2115:52:39.13820964-20997/com.common.coroutines_retrofit_okhttp D/MainActivity:currentThreadisThread[DefaultDispatcher-worker-1,5,main]

這里可能會有人有疑問,因為協程在工作線程執行,工作線程本身就不會阻塞主線程,為了進一步驗證這種方式創建了非阻塞的協程,在協程的創建時指定協程執行在主線程。

valcoroutineJob=GlobalScope.launch(Dispatchers.Main){Log.d(TAG,"current Thread is${Thread.currentThread()}")}Log.d(TAG,"GlobalScope.launch create coroutine")

可以看到輸出的日志順序仍然和之前一樣,但是協程執行的線程變成了主線程。從這里可以看出協程并沒有阻塞住主線程的執行。

2020-05-2115:55:59.66422312-22312/com.common.coroutines_retrofit_okhttp D/MainActivity:GlobalScope.launchcreate coroutine2020-05-2115:55:59.69522312-22312/com.common.coroutines_retrofit_okhttp D/MainActivity:currentThreadisThread[main,5,main]

2)runBlocking創建。

返回一個指定的類型,類型由協程任務的返回值控制,阻塞式創建,這種方式會阻塞住創建協程的線程,只有協程執行結束才能繼續線程的下一步執行,默認執行在創建協程的線程。

valcoroutine2=runBlocking{Log.d(TAG,"current Thread is${Thread.currentThread()}")}Log.d(TAG,"runBlocking create coroutine")

從日志輸出可以看到在協程執行完畢,主線程的日志才進行打印。

2020-05-2115:57:27.92722781-22781/com.common.coroutines_retrofit_okhttp D/MainActivity:currentThreadisThread[main,5,main]2020-05-2115:57:27.92722781-22781/com.common.coroutines_retrofit_okhttp D/MainActivity:runBlocking create coroutine

為了進一步驗證阻塞性,指定runBlocking創建的協程在工作線程執行,并且在協程中模擬一個耗時任務。

valcoroutine2=runBlocking(Dispatchers.IO){Log.d(TAG,"current Thread is${Thread.currentThread()}")delay(5000)}Log.d(TAG,"runBlocking create coroutine")

從日志中可以看到協程執行在工作線程,但是主線程仍然等待5秒,等待協程執行完畢。

2020-05-2115:58:47.50623031-23106/com.common.coroutines_retrofit_okhttp D/MainActivity:currentThreadisThread[DefaultDispatcher-worker-1,5,main]2020-05-2115:58:52.51623031-23031/com.common.coroutines_retrofit_okhttp D/MainActivity:runBlocking create coroutine

3)async創建。

返回值是Deferred,非阻塞式創建,很類似launch方式。如果當前創建處沒有協程上下文信息也需要使用GlobalScope調用async方法創建,GlobalScope.async和直接用async方式創建的區別和launch是一樣的。主要是特點是處理協程并發,當多個協程在同一個線程執行時,一個協程掛起了,不會阻塞另一個協程執行。

runBlocking{varstartTime=System.currentTimeMillis()valtime=measureTimeMillis{valdeferred1=async{delay(2000L)Log.d(TAG,"deferred1 get result , current thread is{Thread.currentThread()}")}valdeferred2=async{delay(3000L)Log.d(TAG,"deferred2 get result , current thread is{Thread.currentThread()}")}Log.d(TAG,"result is{deferred1.await()+deferred2.await()}")}Log.d(TAG,"cost time istime")Log.d(TAG,"cost time2 is${System.currentTimeMillis()-startTime}")}

從日志中可以看出兩個協程執行總耗時大概3s中,并不是兩個協程總體延遲5s,說明在第一個協程掛起進行延時的時候,第二個協程已開始調度執行。并且兩個協程都是在runBlocking所在的主線程中執行

2020-05-2116:00:23.53423638-23638/com.common.coroutines_retrofit_okhttp D/MainActivity:deferred1getresult,current threadisThread[main,5,main]2020-05-2116:00:24.53623638-23638/com.common.coroutines_retrofit_okhttp D/MainActivity:deferred2getresult,current threadisThread[main,5,main]2020-05-2116:00:24.53823638-23638/com.common.coroutines_retrofit_okhttp D/MainActivity:resultis1502020-05-2116:00:24.53923638-23638/com.common.coroutines_retrofit_okhttp D/MainActivity:cost timeis30112020-05-2116:00:24.53923638-23638/com.common.coroutines_retrofit_okhttp D/MainActivity:cost time2is3012

2.協程可以嵌套使用。

父子協程來執行不同的任務。在協程的嵌套中子協程可以省略GlobalScope,直接調用launch和async就可以進行創建,這樣直接共用父協程的作用域,在父協程所在的線程執行。也可以通過Dispatchers指定作用的線程。GlobalScope其實是協程的作用域,協程的執行必須有作用域,這個后面會講解到。這里舉一個最簡單的嵌套的例子。

runBlocking { launch { Log.d(TAG, "launch current Thread is {Thread.currentThread()}") } Log.d(TAG, "current Thread is{Thread.currentThread()}") }

2020-05-2116:02:11.16124076-24076/com.common.coroutines_retrofit_okhttp D/MainActivity:currentThreadisThread[main,5,main]2020-05-2116:02:11.16224076-24076/com.common.coroutines_retrofit_okhttp D/MainActivity:launch currentThreadisThread[main,5,main]

可以看到runBlocking內部通過launch又創建了一個協程,并且launch使用runBlocking的協程上下文在主線程中執行。

協程嵌套有幾個需要注意的點:

1)父協程取消執行的時候,子協程也會被取消執行。

2)父協程總是會等待子協程執行結束。

3.掛起函數

說起協程就必須講掛起函數的概念,掛起函數是實現協程機制的基礎,Kotlin中通過suspend關鍵字聲明掛起函數,掛起函數只能在協程中執行,或者在別的掛起函數中執行。delay就是一個掛起函數,掛起函數會掛起當前協程。協程會等待掛起函數執行完畢再繼續執行其余任務。

privatesuspendfundoWork(){Log.d(TAG,"doWork start")delay(5000)Log.d(TAG,"doWork end")}

這里定義一個掛起函數,打印兩行日志,在這兩行日志之間調用delay掛起函數掛起協程5s中。

2020-05-2116:04:40.02225119-25119/?D/MainActivity:doWork start2020-05-2116:04:45.02525119-25119/?D/MainActivity:doWorkend

三、協程取消與超時

1.協程取消。

協程提供了取消操作,如果一個協程任務未執行完畢,但是執行結果已經不需要了,這時可以調用cancel函數取消協程,也可以調用cancelAndJoin方法取消協程并等待任務結束,相當于調用cancel然后調用join。

runBlocking{valjob=launch{delay(500)Log.d(TAG,"launch running Coroutines")}Log.d(TAG,"waiting launch running")job.cancelAndJoin()Log.d(TAG,"runBlocking running end")}

2.超時處理

協程在執行中可能超過預期的執行時間,這時候就需要取消協程的執行,協程提供了withTimeout函數來處理超時的情況,但是withTimeout函數在超時的時候會拋出異常TimeoutCancellationException,可以選擇捕獲這個異常。協程也提供了withTimeoutOrNull函數并返回null來替代拋出異常。

/**

  • 添加超時處理

  • withTimeout

*/funtimeOutCoroutines()=runBlocking{withTimeout(1300L){repeat(1000){i->Log.d(TAG,"I'm sleeping$i...")delay(500L)}}}

四、協程調度器與作用域

1.協程調度器

協程上下文包含一個協程調度器,即CoroutineDispatcher,它確定了哪些線程或與線程相對應的協程執行。協程調度器可以將協程限制在一個特定的線程執行,或將它分派到一個線程池,亦或是讓它不受限地運行。所有的協程構建器諸如 launch 和 async 接收一個可選的 CoroutineContext 參數,它可以被用來顯式的為一個新協程或其它上下文元素指定一個調度器。

/**

  • 協程上下文(實際控制協程在那個線程執行)

  • launch和async都可接收CoroutineContext函數控制協程執行的線程

  • Dispatchers.Unconfined一種特殊的調度器(非受限調度器),運行在默認的調度者線程,掛起后恢復在默認的執行者kotlinx.coroutines.DefaultExecutor中執行

  • Dispatchers.Default 默認調度器,采用后臺共享的線程池(不傳上下文,默認采用這種)

  • newSingleThreadContext 單獨生成一個線程

  • Dispatchers.IO IO線程

*/funcoroutineConetxt()=runBlocking{launch{// 運行在父協程的上下文中,即 runBlocking 主協程Log.d(TAG,"Im working in thread {Thread.currentThread().name}")}launch(Dispatchers.Unconfined){// 不受限的——將工作在主線程中Log.d(TAG,"Unconfined before I'm working in thread{Thread.currentThread().name}")delay(500)Log.d(TAG,"Unconfined after I'm working in thread {Thread.currentThread().name}")}launch(Dispatchers.Default){// 將會獲取默認調度器Log.d(TAG,"Default I'm working in thread{Thread.currentThread().name}")}launch(newSingleThreadContext("MyOwnThread")){// 將使它獲得一個新的線程Log.d(TAG,"newSingleThreadContext I'm working in thread {Thread.currentThread().name}")}launch(Dispatchers.IO){Log.d(TAG,"IO I'm working in thread{Thread.currentThread().name}")}}

2020-05-2116:06:32.75225509-25509/com.common.coroutines_retrofit_okhttp D/MainActivity:Unconfined before I'mworkinginthread main2020-05-2116:06:32.76425509-25553/com.common.coroutines_retrofit_okhttp D/MainActivity:Default I'mworkinginthread DefaultDispatcher-worker-12020-05-2116:06:32.76625509-25555/com.common.coroutines_retrofit_okhttp D/MainActivity:newSingleThreadContext I'mworkinginthread MyOwnThread2020-05-2116:06:32.76625509-25553/com.common.coroutines_retrofit_okhttp D/MainActivity:IO I'mworkinginthread DefaultDispatcher-worker-12020-05-2116:06:32.76625509-25509/com.common.coroutines_retrofit_okhttp D/MainActivity:Im workinginthread main2020-05-2116:06:33.25525509-25552/com.common.coroutines_retrofit_okhttp D/MainActivity:Unconfined after I'mworkinginthread kotlinx.coroutines.DefaultExecutor

從日志輸出可以看到。

1)launch默認在調用的協程上下文中執行,即runBlocking所在的主線程。

2)Dispatchers.Unconfined在調用線程啟動以一個協程,掛起之后再次恢復執行在默認的執行者kotlinx

.coroutines.DefaultExecutor線程中執行。

3)Dispatchers.Default默認調度器,開啟新線程執行協程。

4)Dispatchers.IO創建在IO線程執行。

5)newSingleThreadContext創建一個獨立的線程執行。

如果需要在協程中控制和切換部分任務執行所在的線程,可通過withContext關鍵字。withContext關鍵字接收的也是協程調度器,由此控制切換任務所在線程。

/**

  • withContext 線程切換

*/funswitchThread()=runBlocking{launch{Log.d(TAG,"start in thread{Thread.currentThread().name}")valjob=withContext(Dispatchers.IO){delay(5000)Log.d(TAG,"I'm working in thread{Thread.currentThread().name}")}Log.d(TAG,"end in thread${Thread.currentThread().name}")}}

2020-05-2116:07:55.22525723-25723/com.common.coroutines_retrofit_okhttp D/MainActivity:startinthread main2020-05-2116:08:00.23925723-25796/com.common.coroutines_retrofit_okhttp D/MainActivity:I'mworkinginthread DefaultDispatcher-worker-12020-05-2116:08:00.24025723-25723/com.common.coroutines_retrofit_okhttp D/MainActivity:endinthread main

從日志輸出可以看到withContext將任務調度到IO線程執行。

協程作用域

協程都有自己的作用域(CoroutineScope),協程調度器是在協程作用域上的擴展,協程的執行需要由作用域控制。除了由不同的構建器提供協程作用域之外,還可以使用coroutineScope構建器聲明自己的作用域。它會創建一個協程作用域并且在所有已啟動子協程執行完畢之前不會結束。runBlocking 與 coroutineScope 可能看起來很類似,因為它們都會等待其協程體以及所有子協程結束。 這兩者的主要區別在于,runBlocking 方法會阻塞當前線程來等待, 而 coroutineScope 只是掛起,會釋放底層線程用于其他用途。 由于存在這點差異,runBlocking 是常規函數,而 coroutineScope 是掛起函數。

/**

  • 協程作用域 coroutineScope創建協程作用域

  • runBlocking會等待協程作用域內執行結束

*/funmakeCoroutineScope()=runBlocking{launch{Log.d(TAG,"launch current Thread is{Thread.currentThread()}")}coroutineScope{// 創建一個協程作用域launch{Log.d(TAG,"coroutineScope launch current Thread is{Thread.currentThread()}")}Log.d(TAG,"coroutineScope current Thread is{Thread.currentThread()}")}Log.d(TAG,"runBlocking current Thread is{Thread.currentThread()}")}

五、應用

從以上分析應該知道協程可以用來做什么了,協程可用來處理異步任務,如網絡請求、讀寫文件等,可以用編寫同步代碼的方式來完成異步的調用,省去了各種網絡、異步的回調。這里做一個最簡單的網絡請求的例子,使用Retrofit+Okhttp請求網絡數據,然后用Glide加載請求回來的圖片。以前寫網絡請求的時候往往封裝一套RxJava+Retrofit+Okhttp來處理,這里將RxJava替換成Coroutines(協程)。

image

image

主要看請求網絡相關的代碼。

classMainViewModel:ViewModel(){companionobject{constvalTAG="MainViewModel"}privatevalmainScope=MainScope()privatevalrepertory:MainRepositorybylazy{MainRepository()}vardata:MutableLiveData<JsonBean>=MutableLiveData()fungetDataFromServer()=mainScope.launch{valjsonBeanList=withContext(Dispatchers.IO){Log.d(TAG,"${Thread.currentThread()}")repertory.getDataFromServer()}data.postValue(jsonBeanList)}overridefunonCleared(){super.onCleared()mainScope.cancel()}}

使用了MainScope來引入協程作用域,在這里跟正常使用GlobalScope.launch來創建運行在主線程的協程是一樣的,然后在協程中通過withContext開啟IO線程執行聯網請求。

classMainRepository{suspendfungetDataFromServer():JsonBean{returnRetrofitRequest.instance.retrofitService.json()}}

classRetrofitRequestprivateconstructor(){privatevalretrofit:Retrofitbylazy{Retrofit.Builder().client(RetrofitUtil.genericClient()).addConverterFactory(GsonConverterFactory.create()).baseUrl(RetrofitUtil.baseUrl).addCallAdapterFactory(CoroutineCallAdapterFactory()).build()}valretrofitService:RetrofitServicebylazy{retrofit.create(RetrofitService::class.java)}companionobject{valinstance:RetrofitRequestbylazy(mode=LazyThreadSafetyMode.SYNCHRONIZED){RetrofitRequest()}}}

interfaceRetrofitService{@GET(Api.json)suspendfunjson():JsonBean}

這里導入了JakeWharton大神編寫的retrofit2-kotlin-coroutines-adapter適配器來做轉換,替換之前的Retrofit轉RxJava的適配器。可以看到處理線程切換只需要withContext一行代碼,并且沒有類似CallBack的回調,整體代碼編寫就是同步代碼的方式。之前使用RxJava的時候還需要對RxJava鏈式請求進行一些封裝來完成網絡請求的CallBack。代碼如下:

fun<T>Observable<T>.parse(success:(T)->Unit){this.subscribeOn(Schedulers.io()).unsubscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(object:Subscriber<T>(){overridefunonNext(t:T){success(t)}overridefunonCompleted(){}overridefunonError(e:Throwable?){}})}

創建了一個Observable的擴展函數parse,通過success函數將網絡請求結果回傳到界面層,相比RxJava協程不需要進行添加CallBack。

Demo地址:

Coroutines

https://github.com/24KWYL/Coroutines-Retrofit-Okhttp

RxJava

https://github.com/24KWYL/MVVM

六、總結

通過協程可以很方便的處理異步任務,可以用同步的方式處理異步請求,減少回調代碼。協程也提供Flow、Channel等操作,類似于RxJava的流式操作。功能上在很多地方可以替換RxJava,也可以實現RxJava的多種操作符。并且使用上更加簡單。

作者:24k金

鏈接:http://www.lxweimin.com/p/acbd9bf65063

來源:簡書

著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容

  • 一、什么是Coroutines(協程) 協程是很久之前就提出的一個概念,目前支持協程的語言包括 lua、C#、go...
    24k金閱讀 2,347評論 0 0
  • 協程(Coroutine) 協程引入 異步加載圖片 普通代碼:val view = ...loadImageAsy...
    晨起清風閱讀 1,290評論 0 1
  • 我是在深入學習 kotlin 時第一次看到協程,作為傳統線程模型的進化版,雖說協程這個概念幾十年前就有了,但是協程...
    前行的烏龜閱讀 99,968評論 32 182
  • 我是在深入學習 kotlin 時第一次看到協程,作為傳統線程模型的進化版,雖說協程這個概念幾十年前就有了,但是協程...
    private_object閱讀 599評論 0 1
  • 關鍵詞:Kotlin 協程 協程取消 任務停止 協程的任務的取消需要靠協程內部調用的協作支持,這就類似于我們線程中...
    Kotlin中文社區閱讀 3,302評論 0 8