天天看點

Kotlin語言中協程(Coroutines)的使用

寫在前面

  • 什麼是協程(coroutines)

    協程是一種類似于輕量級,更高效的線程(實際上它不是線程).為什麼說它輕量級且高效呢,因為它實際上還是在目前線程中操作,但是它執行任務時又不會阻塞目前線程,是以它沒有切換線程帶來的額外資源消耗,實際開發中你你能開啟的線程數量是有限的,并且線程是由作業系統控制的.但協程隻要你的CPU和記憶體資源足夠,你完全可以開啟100000個協程,并且每個協程都由你自己來操作,你可以決定何時關閉或者開啟協程.

    協程并不是kotlin語言發明和創造的,在C#,python,go等語言都有協程,C++也可以實作協程(微信背景伺服器的C++代碼就使用了微信自己開發的協程庫,因為使用協程替代了多線程,并發能力得到百倍的提升)

  • 為何要使用協程

    更高效,上面已經解釋了,協程比線程更高效,并且可控性更強,而且更為安全,因為實際都在同一個線程中運作,不會存線上程安全性問題.最最重要的是,協程寫起來更加簡單邏輯了解起來也更加容易,不需要正常的異步操作那樣不斷地回調,最後進入回調地獄(callback hell)

    在Android開發中,google提供了Android對kotlin協程的支援庫,并且在IO開發者大會上強烈推薦使用kotlin的coroutines進行IO操作,在Android中如何使用kotlin協程請求網絡或者讀取資料庫等異步操作,我在後面的部落格中會介紹,今天主要看看協程的一些基本概念和使用

如何使用協程

首先保證你的kotlin版本是1.3以上,1.3以上coroutines存在于标準庫中了

通過代碼來看看coroutines具體怎麼使用

  • 配置環境

    使用intellij idea 2018.3.3(順便說一句,作為一個Android開發者,最好也要裝一個idea,idea不僅具備Android studio所有功能,并且還能開發後端,還能開發前端,開發gradle,有助于了解前後端以及gradle編譯原理等),建立一個project,選擇gradle項目,右邊選擇kotlin(Java),後面根據需要填寫,一路next直至建立完成

    打開項目根目錄的build.gradle檔案

    plugins {
        id 'org.jetbrains.kotlin.jvm' version '1.3.11'
    }	
               

    確定version在1.3以上

    在dependencies 中添加以下這句(标準的kotlin中隻含有suspend關鍵字,不包含launch,async/await 等功能,是以需要添加以下支援庫)

    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
               
    如果是在Android項目中使用,還需要添加以下
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
               
  • suspend函數和runblocking

    建立一個src目錄下建立一個kotlin檔案main.kt

    首先我們來看一下做一個普通的延遲列印,用線程的方式是怎麼做的

    fun main(args: Array<String>) {
       	 exampleBlocking()
    }
    /**
     * 列印資訊和目前時間戳的最後四位
     */
    fun printlnWithTime(message: String){
        println("$message -- ${System.currentTimeMillis().toString().takeLast(4)}")
    }
    
    fun printlnDelayed(message: String) {
    	//目前線程延遲1000毫秒,這會阻塞目前線程
        Thread.sleep(1000)
        printlnWithTime(message)
    }
    
    fun exampleBlocking() {
        printlnWithTime("one")
        printlnDelayed("two")
        printlnWithTime("three")
    }
               

上述代碼運作的結果是

one -- 4629
two -- 5649
three -- 5649

Process finished with exit code 0
           

第二次列印和第一次列印延遲1000毫秒

再來看看用協程的方式怎麼做的

import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking

fun main(args: Array<String>) {
    exampleBlocking()
}
/**
* 列印資訊和目前時間戳的最後四位
*/
fun printlnWithTime(message: String){
	println("$message -- ${System.currentTimeMillis().toString().takeLast(4)}")
}

//suspend表示目前函數為挂起函數,隻能在CoroutineScope或者另一個suspend函數中調用
suspend fun printlnDelayed(message: String) {
	//表示延遲1000毫秒,delay同樣是suspend函數
    delay(1000)
    printlnWithTime(message)
}
//runBlocking 後面的閉包就是執行在CoroutineScope中,是以可以在其中直接調用suspend函數
fun exampleBlocking() = runBlocking {
    printlnWithTime("one")
    printlnDelayed("two")
    printlnWithTime("three")
}
           

上述代碼執行的結果是:

one -- 1946
two -- 2961
three -- 2962

Process finished with exit code 0
           

結果一緻,第二次列印和第一次列印間隔1秒

上面的代碼中delay()為suspend函數(挂起函數),suspend函數隻能在CoroutineScope(協程作用域)中,或者另外一個suspend函數中調用,是以我們定義printlnDelayed()函數也為suspend函數.

runblocking會啟動一個協程并且阻塞目前線程一直到其中所有的協程都執行完畢.

runblocking後面的閉包就是在CoroutineScope中,是以可以在其中直接調用suspend函數.

  • CoroutineDispatcher 協程排程器

将上述代碼中exampleBlocking()方法改成以下方法

fun exampleBlockingDispatcher() {
    runBlocking(Dispatchers.Default) {
        printlnWithTime("one - from thread ${Thread.currentThread().name}")
        printlnDelayed("two - from thread ${Thread.currentThread().name}")
    }
    printlnWithTime("three - from thread ${Thread.currentThread().name}")
}
           

執行結果如下

one - from thread DefaultDispatcher-worker-1 -- 2970
two - from thread DefaultDispatcher-worker-1 -- 3985
three - from thread main -- 3987

Process finished with exit code 0
           

one和two之間仍然是間隔1000毫秒執行,但是,one,two在worker-1線程中執行,而three仍然在main線程執行.并且three也是在runblocking全部執行完了才執行,也正好印證了我們剛才說的runblocking會阻塞目前線程(即使是在另一個線程執行協程).

通過源碼我們知道,runblocking有兩個參數,第一個參數是CoroutineContext(協程上下文)是可選參數,第二個參數就是後面大括号的方法.Dispatchers.Default的類型是CoroutineDispatcher(協程排程器)就是CoroutineContext的子類,協程排程器可以将協程的執行局限在指定的線程中,這在Android開發中很有用,後面的部落格會詳述.

  • launch函數

    将上面的方法exampleBlockingDispatcher()改成如下代碼

fun exampleLaunch() = runBlocking {
    printlnWithTime("one - from thread ${Thread.currentThread().name}")
    //launch表示啟動一個新的協程,但不阻塞目前線程
    launch {
        printlnDelayed("two - from thread ${Thread.currentThread().name}")
    }
    printlnWithTime("three - from thread ${Thread.currentThread().name}")
}
           

執行結果如下:

one - from thread main -- 9551
three - from thread main -- 9563

Process finished with exit code 0
           

你可能會奇怪了,two跑哪裡去了,怎麼沒列印two,是launch裡面的方法沒執行嗎?其實不是,程式先執行了one,然後執行了launch.launch表示啟動一個新的協程并且不阻塞目前線程,launch後面的閉包是執行在在CoroutineScope中,但請注意launch本身和runblocking一樣不是suspend函數.launch啟動一個協程可能需要幾毫秒,但是程式不會暫停,程式會接着執行three,當執行完了three,runblocking會判斷,我閉包中的所有代碼都執行了(因為launch函數也執行了,盡管launch完全執行所有代碼需要時間,但是launch不是suspend函數,runblocking不會等待非suspend函數),我該結束了,于是,它結束了程式,而launch閉包中的延時還沒來得及執行,程式就被關閉了,是以two沒有被列印出來.

那麼我們想讓two列印出來呢,我們隻需要延遲一下程式關閉,讓launch有時間執行完所有代碼

fun exampleLaunch() = runBlocking {
    printlnWithTime("one - from thread ${Thread.currentThread().name}")
    launch {
        printlnDelayed("two - from thread ${Thread.currentThread().name}")
    }
    printlnWithTime("three - from thread ${Thread.currentThread().name}")
    //延遲以等待launch中的代碼執行完畢
    delay(3000)
}
           

執行結果如下:

one - from thread main -- 7919
three - from thread main -- 7931
two - from thread main -- 8940

Process finished with exit code 0
           

這樣two在大約1秒之後被列印了

launch也可以和runblocking一樣配置協程排程器,當調用 launch { …… } 時不傳參數,它從啟動了它的 CoroutineScope 中承襲了上下文(以及排程器)。在這個案例中,它從 main 線程中的 runBlocking 主協程承襲了上下文。如果我配置了launch的排程器,它會從指定的線程中執行,代碼如下:

fun exampleLaunch() = runBlocking {
    printlnWithTime("one - from thread ${Thread.currentThread().name}")
    launch(Dispatchers.Default) {
        printlnDelayed("two - from thread ${Thread.currentThread().name}")
    }
    printlnWithTime("three - from thread ${Thread.currentThread().name}")
    delay(3000)
}
           

執行結果如下:

one - from thread main -- 8182
three - from thread main -- 8195
two - from thread DefaultDispatcher-worker-2 -- 9201

Process finished with exit code 0
           

以上two在另外一個線程中執行了.

請注意,launch函數是CoroutineScope的方法,如果要調用launch必須要有CoroutineScope對象,上面代碼的launch{…} 其實是 this.launch{…},而這個this是runblocking閉包中的CoroutineScope對象

如果沒有CoroutineScope對象,可以使用GlobalScope.launch{…},GlobalScope是一個全局的CoroutineScope對象

  • 使用Job控制協程

有沒有發現runblocking最後一行的delay實在太low了,萬一我launch中的代碼執行時間不确定怎麼辦,那我怎麼知道要delay多久.那我如何控制我的協程呢,将上面的exampleLaunch()方法改成如下代碼:

fun exampleLaunchWait() = runBlocking {
    printlnWithTime("one - from thread ${Thread.currentThread().name}")
    //launch會傳回一個Job對象,該對象代表新啟動的協程,可以通過job來操作協程
    val job = launch {
        printlnDelayed("two - from thread ${Thread.currentThread().name}")
    }
    printlnWithTime("three - from thread ${Thread.currentThread().name}")
    //join()是suspend函數,它的作用是挂起目前協程,直到job完成
    job.join()
}
           

執行結果如下

one - from thread main -- 7976
three - from thread main -- 7989
two - from thread main -- 8999

Process finished with exit code 0
           

Job在Android 開發中很有用,比如如果你要在Activity結束時關閉異步任務,可以在onDestory()中調用job.cancel() ,雖然實際開發中為了架構的解耦性和可測試性,我們一般不會在Activity或者Fragment中直接處理資料,但是job.cancel()仍然可以使用在ViewModel中,關于這一塊,後面的部落格會詳解.

  • async和withContext

    async和launch類似,都是建立一個新的協程(請注意,launch建立一個新的協程之後會立即啟動,預設情況下,async也會立即啟動,但也可以設定惰性啟動),不同的是.launch{…}傳回Job,async{…}傳回Deferred,Deferred的await()方法可以立即獲得協程的結果.先看代碼:

import kotlinx.coroutines.*
fun main(args: Array<String>) {
    exampleAsyncAwait()
}
/**
 * 模拟複雜計算
 */
suspend fun calculateHardThings(startNum: Int): Int {
    delay(1000)
    return startNum * 10
}

fun exampleAsyncAwait() = runBlocking {
    val startTime = System.currentTimeMillis()
    //啟動一個新的協程,用于計算結果
    val deferred1 = async { calculateHardThings(10) }
    val deferred2 = async { calculateHardThings(20) }
    val deferred3 = async { calculateHardThings(30) }
    //await會阻塞目前線程,等待計算完畢,并且傳回協程的計算結果
    val sum = deferred1.await() + deferred2.await() + deferred3.await()
    println("sum = $sum")
    val endTime = System.currentTimeMillis()
    println("總耗時: ${endTime - startTime}")
}
           

執行結果如下:

sum = 600
總耗時: 1016

Process finished with exit code 0
           

async不會阻塞線程,但是await會阻塞線程,因為是啟動了三個協程用于分别計算,是以await的時間也就是1秒左右,對比一下以下代碼

//反面例子
fun exampleAsyncAwait() = runBlocking {
    val startTime = System.currentTimeMillis()
    //await将會阻塞目前線程,直至async中協程執行完畢了才會放行
    val deferred1 = async { calculateHardThings(10) }.await()
    val deferred2 = async { calculateHardThings(20) }.await()
    val deferred3 = async { calculateHardThings(30) }.await()
    val sum = deferred1 + deferred2 + deferred3
    println("sum = $sum")
    val endTime = System.currentTimeMillis()
    println("總耗時: ${endTime - startTime}")
}
           

執行結果:

sum = 600
總耗時: 3025

Process finished with exit code 0
           

await将會阻塞目前線程,直至async中協程執行完畢了才會放行,相當于計算一個接着才能計算下一個,是以總耗時3秒,這當然是不好的,這是反面例子.

async适用于需要多次調用并且需要知道結果的場景,比如多任務下載下傳.如果我不需要多次調用隻需要知道結果,還可以使用withContext,withContext的效果類似于上面代碼的async{…}.await(),不同的是withContext沒有預設排程器必須要指定一個協程排程器,代碼如下:

fun exampleWithContext() = runBlocking {
    val startTime = System.currentTimeMillis()
    val result1 = withContext(Dispatchers.Default) { calculateHardThings(10) }
    val result2 = withContext(Dispatchers.Default) { calculateHardThings(20) }
    val result3 = withContext(Dispatchers.Default) { calculateHardThings(30) }
    val sum = result1 + result2 + result3
    println("sum = $sum")
    val endTime = System.currentTimeMillis()
    println("總耗時: ${endTime - startTime}")
}
           

執行結果:

sum = 600
總耗時: 3020

Process finished with exit code 0
           

withContext很适合需要直接擷取協程結果但又不會短時間内重複調用的場景,比如網絡請求.

好了,基本上常用的協程使用方法都介紹了,當然協程還有更多的使用方式方法,本文隻是抛磚引玉

最後,還記得當年學習線程的時候賣火車票的例子嗎,我們用協程也來實作一個,如果你能獨立實作,說明你已經了解了協程的用法了,代碼如下:

import kotlinx.coroutines.*
import kotlin.random.Random

fun main(args: Array<String>) {
    saleTickets()
}

fun saleTickets() = runBlocking {
    //要賣出的票總數
    var ticketsCount = 100
    //售票員數量
    val salerCount = 4
    //此清單儲存async傳回值狀态,用于控制協程等待
    val salers: MutableList<Deferred<Unit>> = mutableListOf()
    repeat(salerCount) {
        val deferred = async {
            while (ticketsCount > 0) {
                println("第${it + 1}個售票員 賣出第${100 - ticketsCount + 1}張火車票")
                ticketsCount--
                //随機延遲100-1000毫秒,使每次售票時間不相同
                val random = Random.nextInt(10)+1
                delay((random * 100).toLong())
            }
        }
        salers.add(deferred)
    }
    salers.forEach { it.await() }
}
           

轉載請注明出處