Kotlin coroutine之協程基礎

fb0122發表於2018-10-31

圖片來自必應

本文是對官方文件中協程的教程的翻譯加上個人理解,也可以直接閱讀官方文件:Your first coroutine with Kotlin

協程可以認為是一個輕量級的執行緒,和執行緒一樣,它可以同時執行、等待執行或者馬上執行。它與執行緒最大的不同在於協程的開銷非常低,幾乎不需要開銷。我們可以建立數千個協程,並且只付出很少的效能損耗。從另一方面來說,真正的執行緒去開啟並且執行它是十分昂貴的,數千個執行緒對現代機器的效能來說是個十分嚴峻的挑戰。

  • 引入協程

引入協程的方法很簡單,只需要在app的build.gradle 檔案中引入coroutine支援:

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0'
}
複製程式碼

那麼我們如何開始使用協程? 讓我們來看看 lunch{}函式:

GlobalScope.lunch{
    ...
}
複製程式碼

上述程式碼將會開啟一個新的協程, GlobalScope 表示該協程的生命週期僅受整個應用的生命週期影響。當然我們也可以建立一個新的協程基於某一個執行緒的生命週期,例如:

CoroutineScope(newSingleThreadContext("thread-1")).launch {  }
複製程式碼

在預設情況下,協程會執行線上程共享池。在基於協程的程式中執行緒仍然會存在,但是一個執行緒能夠執行很多協程,所以我們並不需要建立很多的執行緒, 我們來看使用協程的一整段程式碼:

fun main(args: Array<String>){
    println("Start")
    
    GlobalScope.launch{
        delay(1000)
        println("Hello")
    }
    
    Thread.sleep(2000)
    println("Stop")
}
複製程式碼

從上面的程式碼可以看出來,我們可以使用delay() 函式類似Thread() 類中的 sleep() 方法,但是這個方法的好處在於:它並不會像Thread().sleep() 那樣會阻塞執行緒,而僅僅是暫停當前的協程。當協程暫停的時候,當前的執行緒將會釋放,當協程暫停結束的時候,它將會線上程池中的空閒的執行緒上恢復,這樣就意味著,如果我們使用協程,我們就可以不用像執行緒那樣去使用回撥處理返回結果,雖然RxJava可以做到等待結果返回,但是也沒有協程這樣方便簡潔

如果在主執行緒的話,必須要等待我們協程完成,否則上面的例子將會在“Hello”列印之前結束了。

讓我們將上面的Thread().sleep(2000) 這句程式碼註釋掉,那麼結果將會是先列印“Stop”,再列印“Hello”。

如果我們直接使用同樣的非阻塞方法 delay() 在主執行緒內,將會出現編譯錯誤:

Suspend functions are only allowed to be called from a coroutine or another suspend function

這個錯誤是因為我們使用了delay( ) 而沒有在任何的協程中,我們可以通過runBlocking{} 來啟動一個協程並且阻塞直到其完成:

runBlocking{
    delay(1000)
}
複製程式碼

現在,對於上面的例子來說,首先會列印“Start” ,然後會執行到launch{},然後會執行runBlocking{} 直到它完成,然後列印“Stop”,與此同時第一個協程完成並且列印“Hello”。

  • async: 返回協程的值

還有一種開啟一個協程的方法為async{}, 在這方面它和launch{} 具有一樣的效果,但是async{}會返回一個Deferred<T> 的例項,這個例項有一個方法await(),這個方法可以返回協程的結果。

讓我們再來看一段程式碼,先來執行一百萬個協程,並且將它們返回的Deferred物件儲存起來。然後計算結果:

val deferred = (1..1_000_000).map{
    n -> async{
        n
    }
}
複製程式碼

當所有的都啟動了之後,我們顯然需要收集它們的結果:

val sum = deferred.sumBy{it.await()}
複製程式碼

上面的程式碼看上去好像沒有什麼問題,我們把每個協程的結果拿到之後對其求和,看上去好像一切正常,但是編譯器卻報錯了:

Suspend functions are only allowed to be called from a coroutine or another suspend function
複製程式碼

顯然,await() 不能夠被使用在協程之外,因為await() 會暫停協程知道它完成,然而只有協程能夠被不阻塞的暫停,所以,我們應該將await() 寫在協程裡面:

runBlocking{
    val sum = deferred.sumBy{it.await()}
    println("Sum: $sum")
}
複製程式碼
  • 掛起函式(Suspending functions)

正如文章開頭提到的,協程最大的優點就是可以不通過阻塞而掛起執行緒,編譯器必須要通過一些特殊的程式碼而去實現這個功能,所以我們必須要顯式的說明那些可能會掛起(suspend)的程式碼,所以可以使用suspend去說明:

suspend fun workload(n: Int): Int{
    delay(1000)
    return n
}
複製程式碼

當我們使用suspend顯式的說明workload() 函式可能會suspend之後,當我們從協程中呼叫它,編譯器就會知道這個函式將會suspend並且做好相應的準備:

async{
    workload(n)
}
複製程式碼

這時workload(n) 將能夠從協程或者其他的suspend函式中呼叫,但是不能夠在協程以外呼叫。相應的,delay()await() 是被預設宣告為suspend的, 這就是為什麼必須要在runBlocking{}launch{}async{} 中才能夠呼叫它們的原因。

以上就是kotlin協程中一些基本概念與使用,關於協程的更多用法會在之後的文章中再一一說明。

相關文章