初遇Kotlin協程
初遇Kotlin協程(coroutine)
這篇文章我們將建立協程專案,並用Coroutines編寫相關程式碼。
Kotlin 1.1引入了協程程式,這是一種編寫非同步、非阻塞程式碼(以及其他)的新方法。在這篇文章中,我們將使用kotlinx.coroutines
庫來了解基本的協程寫法,這個庫是對已存的JAVA庫的封裝。
Setting up a project
我們將使用Gradle
來構建專案。
加入庫依賴:
dependencies { ... implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.1" }
這個庫託管在JCenter倉庫中,我們加入倉庫地址:
repositories { jcenter() }
Coroutine第一行程式碼
我們可以認為協程是一種輕量級的執行緒。像執行緒一樣,協程可以並行,可以相互間通訊。和執行緒最大的不同是,協程是非常輕量級的,我們可以建立幾千個協程,但是消耗的效能卻非常非常的少。而對於真的執行緒,這是非常耗資源的。幾千個執行緒對於現代計算機來說是一個嚴重的挑戰。
我們怎麼啟動一個協程呢?可以使用launch{}
函式:
launch{ ... }
完整的例子如下:
println("Start") GlobalScope.launch { delay(1000) println("Hello") } Thread.sleep(2000) // wait for 2 seconds println("Stop")
這裡我們啟動了一個協程,等待一秒後,列印了Hello
。
我們使用了delay()
這個方法,這個方法類似Thread.sleep()
,但是更好,它不阻塞執行緒,只是掛起協程本身。當協程掛起等待是,返回到執行緒;當協程等待完成時,協程恢復繼續執行。
在這個例子中,主執行緒main()
必須等待協程完成,否則在輸出Hello
之前,程式就結束了。
假如我們在main()
中直接使用delay()
函式,將會遇到編譯錯誤:
Suspend functions are only allowed to be called from a coroutine or another suspend function
這是因為掛起函式只能執行在協程中,我們可以使用runBlocking()
來啟動一個協程。
runBlocking { delay(2000) }
複雜一點的例子
讓我們來看下協程是不是輕量級的,我們啟動一百萬個執行緒:
val c = AtomicLong() for (i in 1..1_000_000L) thread(start = true) { c.addAndGet(i) } println(c.get())
這個例子將會執行較長一段時間,消耗較多的資源。:(
我們換種方式,用協程來實現:
val c = AtomicLong() for (i in 1..1_000_000L) GlobalScope.launch { c.addAndGet(i) } println(c.get())
這個例子幾秒就完成了,對比執行緒,有著顯著的優勢。
Async: 從協程中返回值
另一種啟動協程的方法是使用async{}
,它類似launch{}
,但是它返回Deferred<T>
例項,這個例項有await()
方法,該方法返回協程結果。
我們啟動一百萬個協程,然後持有它們的返回結果Deferred
。
val deferred = (1..1_000_000).map { n -> GlobalScope.async { n } }
這些協程都已經啟動,我們把它們加起來。
val sum = deferred.sumBy { it.await() }
我們使用了標準函式庫sumBy
,來把他們加在一起,但是我們簡單這樣做,編譯器會報錯:
Suspend functions are only allowed to be called from a coroutine or another suspend function
因為await()
是掛起函式,不能用在協程外面,正如上面說過的一樣。我們把它放在協程裡:
runBlocking { val sum = deferred.sumBy { it.await() } println("Sum: $sum") }
現在它將會順利輸出結果。我們稍微改下程式碼,確認這個百萬個協程是平行的,假如我們再每個啟動的協程裡延時一秒,看看是否要花費百萬秒才會輸出結果:
val deferred = (1..1_000_000).map { n -> GlobalScope.async { delay(1000) n } }
我們可以執行下,就可以知道結果。結果是隻用了十幾秒。顯然,它是並行的。
Suspending functions
現在我們把裡邊的程式碼提取出來:
fun workload(n: Int): Int { delay(1000) return n }
一個類似的錯誤將會出現:
Suspend functions are only allowed to be called from a coroutine or another suspend function
讓我們進一步看看這個是什麼意思。協程最大的優勢是可以不阻塞執行緒的掛起。編譯器將會組織特殊的程式碼來達到這個可能,所以我們使用suspend
來修飾一個方法:
suspend fun workload(n: Int): Int { delay(1000) return n }
現在我們可以在協程種呼叫workload
方法,編譯器知道這個方法可能會掛起,並做相應的工作。
GlobalScope.async { workload(n) }
我們的workload
方法能夠在協程中被呼叫,或者別的掛起函式,但是不能夠在協程外呼叫。相應的,delay()
和await()
函式被suspend
修飾,這就是為什麼它們只能在掛起函式中被呼叫,或者在協程內被呼叫,runBlocking()
,launch{}
, 或者async()
。