Coroutine 的三大要素不知道大家還記得嗎?CoroutineScope、Suspend function、Dispatchers。
CoroutineScope 是定義 coroutine 執行的範圍,我們可以使用 launch
、 async
來建立範圍。
Suspend function 是用來處理非同步的執行,所以我們可以在這邊放上耗時任務,Coroutine 執行到這邊的時候,就會將此 coroutine 暫停(suspend/掛起),等到完成之後,才又會把此 coroutine 恢復運行。
而 Dispatchers 是用來指定該 coroutine 使用不同的調度器來執行,所謂調度器指的是 coroutine 根據不同的應用會在背景會建立不同的執行緒池/執行緒,使用者可以根據使用的情境來選擇適當的 Dispatchers。如 Dispatchers.IO、Dispatchers.Default...
今天要來介紹的是那些內建的 suspend function,如我們之前經常使用的 delay()
就是其中一個成員喔。
第一個當然就是我們的 delay()
囉,以前我們使用執行緒的時候,如果我們需要在執行緒上暫停一下,我們會使用 Thread.sleep() 來把目前的執行緒停止,不過這樣做的缺點就是整個執行緒就停起來了,這個執行緒就什麼事都不能做。
delay()
則是讓這個 coroutine 進入等待的狀態,coroutine 進入等待之後,就會去找尋在同一個 Dispatchers 的任務來執行。
fun main() = runBlocking<Unit> {
launch {
Thread.sleep(100L)
println("Thread 1 ${Thread.currentThread().name}")
}
launch {
println("Thread 2 ${Thread.currentThread().name}")
}
}
Coroutine 1 main
Coroutine 2 main
→ 在第一個 launch 中呼叫了 Thread.sleep(100L) 之後,執行緒就會暫停 100 毫秒,當暫停時間結束過後,才會開始執行後面的內容。所以輸出會是 Coroutine 1 main → Coroutine 2 main
如果在 coroutine 裏面使用 Thread.sleep()
,會顯示一個警告。
就是在告訴你 coroutine 不要使用會阻塞的方法啦。
delay()
fun main() = runBlocking<Unit> {
launch {
delay(100L)
println("Coroutine 1 ${Thread.currentThread().name}")
}
launch {
println("Coroutine 2 ${Thread.currentThread().name}")
}
}
Coroutine 2 main
Coroutine 1 main
→ 第一個 launch 呼叫 delay()
之後,這一個 launch
的 coroutine 的狀態被切換成等待,然後就會執行下一段程式。當 delay()
結束之後,就會從暫停的地方恢復,所以就會繼續往下執行。最後看到的輸出結果就是 Coroutine 2 main -> Coroutine 1 main
如果直接查字典,可能會得到一個不太貼切的翻譯:屈服。
根據韋氏辭典的解釋,我認為比較貼切的是這個解釋
to give up possession of on claim or demand - 根據主張或需求放棄權利。
那麼,到底 yield()
到底是什麼用途呢?
Yields the thread (or thread pool) of the current coroutine dispatcher to other coroutines on the same dispatcher to run if possible. - Ref
如果可能,放棄目前 coroutine 調度程序的執行緒/執行緒池到另一個在同一個調度器的 coroutine 。
還是很茫嗎?看下面的範例:
launch
產生兩個 coroutine - Ref
fun main() = runBlocking {
val job = launch {
val child = launch {
try {
println("run child")
delay(Long.MAX_VALUE)
} finally {
println("Child is cancelled")
}
}
println("run parent")
yield()
println("Cancelling child")
child.cancel()
child.join()
yield()
println("Parent is not cancelled")
}
job.join()
}
run parent
run child
Cancelling child
Child is cancelled
Parent is not cancelled
→ 從 log 的輸出我們可以發現,我們一開始會先從外側的 coroutine 開始執行 ,所以印出了第一行 run parent
,當我們呼叫 yield()
時,此時執行緒的使用權就會切換至子 coroutine,所以列印出 run child
,接著在子 coroutine 中呼叫 delay()
,內部的 coroutine 切換至等待狀態,並把執行緒使用權切回外層的 coroutine 並列印 Cancelling child
。接著,呼叫 child.cancel()
來把
子 coroutine 裏面的任務給停掉,於是子 coroutine 的 delay()
被取消,列印出 Child is cancelled
。子 coroutine 被取消之後,調用 child.join()
就只會把子 coroutine 切回父 coroutine。下面的 yield()
則因為沒有其他的任務等待,所以沒有作用,最後列印 Parent is not cancelled
結束。
我們換另一個例子來看看:
fun main() = runBlocking{
val job1 = launch {
repeat(10) {
println("coroutine1: $it")
yield()
}
}
val job2 = launch {
repeat(10) {
println("coroutine2: $it")
yield()
}
}
job1.join()
job2.join()
}
猜猜看,這段程式碼會怎麼輸出呢?
如果按照我們前面所說的, yield()
會把放棄目前 coroutine 的執行緒到另一個 coroutine,所以當執行到 yield()
時,就會把執行的權利交給下一個 coroutine 來執行。所以答案會是
coroutine1: 0
coroutine2: 0
coroutine1: 1
coroutine2: 1
coroutine1: 2
coroutine2: 2
coroutine1: 3
coroutine2: 3
coroutine1: 4
coroutine2: 4
coroutine1: 5
coroutine2: 5
coroutine1: 6
coroutine2: 6
coroutine1: 7
coroutine2: 7
coroutine1: 8
coroutine2: 8
coroutine1: 9
coroutine2: 9
另外,假如 coroutine 在暫停的時候被取消,那麼縱使呼叫了 yield()
也沒有辦法回去,畢竟都被取消了。
fun main() = runBlocking {
val job1 = launch {
repeat(10) {
println("coroutine1: $it")
yield()
}
}
val job2 = launch {
repeat(10) {
println("coroutine2: $it")
job1.cancel() //<- Add this line
yield()
}
}
job1.join()
job2.join()
}
coroutine1: 0
coroutine2: 0
coroutine2: 1
coroutine2: 2
coroutine2: 3
coroutine2: 4
coroutine2: 5
coroutine2: 6
coroutine2: 7
coroutine2: 8
coroutine2: 9
在 job2
裏面調用 job1.cancel()
取消 Job1,當在 job2
呼叫 yield()
也沒有辦法切回 Job1。
delay() 與 yield() 使用上的結果看起來有點相像,不過實際的內容是不太一樣的, delay()
是會讓目前的 coroutine 切換成等待狀態,接著就會去尋找下一個等待執行的 coroutine ,因為它只是讓 coroutine 等待,所以執行緒並沒有被停止下來,跟 Thread.sleep()
是不一樣的, Thread.sleep()
會阻塞執行緒,所以後面就算有任務需要執行,也會因為執行緒被卡住的關係而無法執行。
yield()
則是放棄目前執行的權利,讓下一個 coroutine 可以執行(需要同一個調度器),所以就實現來說, yield()
與 delay()
都可以做到暫停目前 coroutine 的任務,不過實際運用上還是有些不同。
內建的 suspend 函式就先介紹這兩個,其他的往後幾篇再繼續介紹。
有興趣的讀者歡迎參考:https://coroutine.kotlin.tips/
天瓏書局