Kotlin學習系列之:協程的創建(三)

經過前面兩篇的學習,我們現在可以來總結一下,我們可以有哪些方式來啓動一個協程:

  • GlobalScope.launch{}
  • runBlocking{}

  1. 接下來我們介紹另外的兩種方式,我們先直接來看代碼:
fun main() = runBlocking {

//    GlobalScope.launch {
//        delay(1000)
//        println("hello coroutine!")
//    }

    launch {
        delay(1000)
        println("hello coroutine!")
    }

    println("hello")
    delay(2000)
    println("world")

}

我們將之前的GlobalScope.launch{}的方式,可以直接替換成launch{}。一運行,我們能夠得到相同的輸出結果:

0s: 打印hello,runBlocking協程開始delay;啓動一個子協程(launch{}起的作用),並且也開始delay

1s: launch子協程delay時間到,打印hello coroutine!

2s: runBlocking協程delay時間到,打印world

那麼這個launch方法是何方神聖呢?我們通過command鍵+鼠標進入到它的方法聲明處,你會發現,它和GlobalScope.launch{}的一樣的,都是CoroutineScope的擴展方法。GlobalScope.launch{}的調用對象是GlobalScope,那麼這裏的launch{}方法的調用對象是誰呢?

這是IntelliJ Idea編輯器給我們的提示,就是在runBlocking{}代碼塊中會有一個CoroutineScope的對象作爲this,實際上不光光是runBlocking{},細心的你肯定會發現上面截圖中的launch{}代碼塊中一樣有這樣的this。至於是爲什麼,我們來看官方文檔的描述:

Every coroutine builder, including runBlocking, adds an instance of CoroutineScope to the scope of its code block. 

翻譯過來就是:對於每一個協程構建器而言,包括runBlocking,都會往它的代碼塊的作用域中添加一個CoroutineScope的實例。換句話說,這個CoroutineScope的實例是由協程構建器注入的。

2. 接下來我們來比較一下GlobalScope.launch{}和.launch{}的不同之處:我們將runBlocking{}中delay(2000)修改成delay(500),然後觀察分別使用GlobalScope.launch{}和.launch{}的運行結果.

fun main() = runBlocking {

   GlobalScope.launch {
       delay(1000)
       println("hello coroutine!")
   }

   println("hello")
   delay(500)
   println("world")
}

hello

(暫停500毫秒)

world

我們會發現,程序在輸出完”world“之後,就退出了。GlobalScope.launch{}中的”hello coroutine“壓根就沒有機會輸出。

fun main() = runBlocking {

   launch {
       delay(1000)
       println("hello coroutine!")
   }

   println("hello")
   delay(500)
   println("world")
}

hello

(暫停500毫秒)

world

(暫停500毫秒)

hello coroutine!

可以看到,使用.launch{}後,在程序輸出完world之後,還會繼續等待launch{}代碼塊中的執行,也就是會輸出”hello coroutine!“,然後才退出程序。

好,現象我們已經看到了,下面來剖析原因:

Every coroutine builder, including runBlocking, adds an instance of CoroutineScope to the scope of its code block. We can launch coroutines in this scope without having to join them explicitly, because an outer coroutine(runBlocking in our example) does not complete until all the coroutines launched in its scope complete.

第一句話我們前面已經翻譯過了,我們來看後面的:

我們可以不需要顯式地join時,在這個作用域下啓動一個協程,因爲外部的協程會等待在其作用域下啓動的所有協程執行完畢後,才標誌着自己地執行完畢。

實際上這裏的協程我們可以稱作是父子協程(外部是父,內部是子),我們後面還會有專門的篇幅來探究父子協程之前的取消、協作等關係。

3. 下面我們來介紹第四種啓動協程的方式:coroutineScope{}

fun main() = runBlocking {
   println("hello")
   coroutineScope {
      delay(1000)
      println("welcome")
   }
   delay(500)
   println("world")
}

hello

(暫停1000毫秒)

welcome

(暫停500毫秒)

world

這裏的輸出結果我們先不去探究,我們來看這個coroutineScope{}是個啥:

public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R =
   suspendCoroutineUninterceptedOrReturn { uCont ->
       val coroutine = ScopeCoroutine(uCont.context, uCont)
       coroutine.startUndispatchedOrReturn(coroutine, block)
   }

這裏會有一個之前沒有接觸過的關鍵字:suspend.對於使用suspend關鍵字修飾的函數我們稱之爲掛起函數(Suspend Function),它有如下特點:它只能用於協程作用域下或者是另一個掛起函數中。比如之前我們一直使用的delay方法,它就是一個掛起函數。再來看看這個coroutineScope的文檔描述:

* Creates a [CoroutineScope] and calls the specified suspend block with this scope.
* The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
* the context's [Job].
*
* This function is designed for _parallel decomposition_ of work. When any child coroutine in this scope fails,
* this scope fails and all the rest of the children are cancelled (for a different behavior see [supervisorScope]).
* This function returns as soon as the given block and all its children coroutines are completed.
* A usage example of a scope looks like this:
*
* ```
* suspend fun showSomeData() = coroutineScope {
*
*   val data = async(Dispatchers.IO) { // <- extension on current scope
*      ... load some UI data for the Main thread ...
*   }
*
*   withContext(Dispatchers.Main) {
*     doSomeWork()
*     val result = data.await()
*     display(result)
*   }
* }

大家可以通讀一下,我這裏就重點關注一句話:

This function returns as soon as the given block and all its children coroutines are completed.

就是說這個函數會在內不能的代碼塊和所有的子協程都執行完畢後纔會返回。有了這句話,我們就能夠輕鬆解釋我們前面示例中的輸出結果。當程序執行到coroutineScope一行之後,會等待其中的子協程和代碼塊執行完畢後,纔會走下面的執行邏輯,所以下面delay(500)以及println("world")都只會在後面得到執行。

4. 我們再次總結一波啓動協程的方式:

  • GlobalScope.launch{}: 創建一個全局的協程
  • runBlocking{}:會阻塞當前線程,一般用於代碼調試中
  • .launch{}: 和GlobalScope.launch{},只不過它是依賴於當前協程作用,而GlobalScope.launch{}是當前線程的全局作用域
  • coroutineScope{}: 稱之爲作用域構建器(scope builder),它是一個掛起函數,它會等待其作用域下的所有代碼塊以及子協程的執行完畢後纔會返回。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章