Kotlin 協程中runBlocking、launch、withContext、async的使用以及區別

步入Kotlin已經蠻久了,它的很多優秀的語法糖真是甜到爆炸,越來越讓開發者進行最少的代碼來做出更多的事情,今天我來記錄了一下創建協程幾個非常重要的關鍵方法,他們之間的區分,用好了你會體會到它的強大的。

這裏做一些簡單的對比:

可以在全局創建協程的方法:runBocking 和lauch 

 lauch 與 runBlocking都能在全局開啓一個協程,但 lauch 是非阻塞的 而 runBlocking 是阻塞的

阻塞和非阻塞的意思是,當前運行的代碼塊是否會阻塞當前線程,我們看一下下面的例子:

當我們使用lauch方式啓動協程的時候,看如下代碼:

CoroutineScope(Dispatchers.Main).launch {
    delay(500)
    Log.e("lcs - ","1.執行CoroutineScope.... [當前線程爲:${Thread.currentThread().name}]")
}
Log.e("lcs - ","2.執行CoroutineScope.... [當前線程爲:${Thread.currentThread().name}]")

輸出結果爲:

可以看到我們協程並沒有阻塞主線程,而是在啓動協程之後直接執行後面的代碼段,所以2.執行 會先打印出來。

我們再看看runBlocking啓動協程,代碼如下:

runBlocking {
    delay(500)
    Log.e("lcs - ","1.執行CoroutineScope.... [當前線程爲:${Thread.currentThread().name}]")
}
Log.e("lcs - ","2.執行CoroutineScope.... [當前線程爲:${Thread.currentThread().name}]")

輸出結果如下:

可以看到我們執行順序是順序執行的,說明已經阻塞了。

大家可以看到這其中的差別了,不過後者我們很少會用到, 由於runBlocking 接收的 lambda 代表着一個 CoroutineScope,所以 runBlocking 協程體內可繼續通過launch來繼續創建一個協程,避免了lauch所在的線程已經運行結束而切不回來的情況。

可接收返回值得協程 withContext和async

這兩種方式我們聲明一個變量來接收,代碼如下:

 

CoroutineScope(Dispatchers.Main).launch {
    val beginTime = System.currentTimeMillis()
    val result1 = withContext(Dispatchers.IO) {
        delay(500)
        Log.e("lcs - ", "result1.... [當前線程爲:${Thread.currentThread().name}]")
        "result1"
    }
    val result2 = withContext(Dispatchers.IO) {
        delay(200)
        Log.e("lcs - ", "result2.... [當前線程爲:${Thread.currentThread().name}]")
        "result2"
    }

    Log.e(
        "lcs",
        "result1 = $result1  , result2 = $result2 , 耗時 ${System.currentTimeMillis() - beginTime} ms  [當前線程爲:${Thread.currentThread().name}]"
    )
}

然後我們看看打印結果是什麼?

我們可以發現時間是兩個任務的和,說明withContext這種方式是串行的,並且逐一返回結果,這是因爲withConext是個 suspend 函數,當運行到 withConext 時所在的協程就會掛起,直到withConext執行完成後再執行下面的方法。所以withConext可以用在一個請求結果依賴另一個請求結果的這種情況,這種情況也是有不少的,有時候我們的需求就是這樣的,一個頁面多個結果,那我們第二個請求要依賴第一個請求的結果的時候我們就可以這樣做。

說到這裏我們也會有這樣的需求,幾個請求之間沒有任何依賴關係的時候,我們怎麼辦?很顯然這個方法就不行了,接下來async、wait就登場了,讓我們看看這東西怎麼實現的呢?

 

CoroutineScope(Dispatchers.Main).launch {
    val beginTime = System.currentTimeMillis()
    val result1 = async(Dispatchers.IO) {
        delay(500)
        Log.e("lcs - ", "result1.... [當前線程爲:${Thread.currentThread().name}]")
        "result1"
    }
    val result2 = async(Dispatchers.IO) {
        delay(200)
        Log.e("lcs - ", "result2.... [當前線程爲:${Thread.currentThread().name}]")
        "result2"
    }
    Log.e(
        "lcs",
        "result1 = ${result1.await()}  , result2 = ${result2.await()} , 耗時 ${System.currentTimeMillis() - beginTime} ms  [當前線程爲:${Thread.currentThread().name}]"
    )
}

運行結果:

運行結果我們發現,時間上少了很多,說明是異步並行的兩個任務,任務2要比任務1先一步執行,所以說 async 的任務都是並行執行的。但事實上有一種情況例外,我們把await()方法的調用提前到 async 的後面,代碼如下:

 

CoroutineScope(Dispatchers.Main).launch {
    val beginTime = System.currentTimeMillis()
    val result1 = async(Dispatchers.IO) {
        delay(500)
        Log.e("lcs - ", "result1.... [當前線程爲:${Thread.currentThread().name}]")
        "result1"
    }.await()
    val result2 = async(Dispatchers.IO) {
        delay(200)
        Log.e("lcs - ", "result2.... [當前線程爲:${Thread.currentThread().name}]")
        "result2"
    }.await()
    Log.e(
        "lcs",
        "result1 = result1  , result2 = result2 , 耗時 ${System.currentTimeMillis() - beginTime} ms  [當前線程爲:${Thread.currentThread().name}]"
    )
}

運行結果如下:

我們發現時間上跟withContext差不多,又回到了串行的時候的情況了,剛只是把await()的位置改了,就出現這樣的結果,所以原因應該就是在await()方法身上,點進 await() 源碼看一下,終於明白了是怎麼一回事,原來await() 僅僅被定義爲 suspend 函數,因此直接在async 後面使用 await() 就和 withContext 一樣,程序運行到這裏就會被掛起直到該函數執行完成纔會繼續執行下一個 async 。但事實上await()也不一定導致協程會被掛起,await() 只有在 async 未執行完成返回結果時,纔會掛起協程。若 async 已經有結果了,await() 則直接獲取其結果並賦值給變量,此時不會掛起協程

簡單的總結就這樣先,我也是看了別人的資料,然後自己親自實踐了一下,這樣不僅有助於記憶和理解,還能夠使用順手。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章