在以前的文章中,已經講了如何啓動協程、協程的做用域是如何組織和工做的以及各類協程構造器(builder)的特性。網絡
本篇將講解對協程的各類操做,包括掛起、取消、超時、切換上下文等。async
fun main() { runBlocking(Dispatchers.Default) { for (i in 0 .. 10) { println("aaaaa ${Thread.currentThread().name}") delay(1000) // 這是一個掛起函數 println("bbbbb ${Thread.currentThread().name}") } } }
delay就是一個掛起函數,掛起的意思是:非阻塞的暫停,與之對應的就是阻塞(的暫停)。好比線程的方法Thread.sleep就是一個阻塞的方法。關於阻塞仍是非阻塞,能夠簡單的理解爲:函數
用圖來表示線程阻塞的狀況應該是這樣:ui
而在協程中,非阻塞的狀況應該是這樣:spa
能夠看到,線程的阻塞,那這個線程就真的不去作事情了,必須等到被喚醒了,纔會繼續執行,在被喚醒以前,這個線程資源能夠說就被浪費了,若是我有新的任務,就必須在啓動一個新的線程來執行。線程
可是協程上的掛起,它會去尋找有沒有須要執行的代碼塊,若是有,就拿來跑,這樣就能更高效的利用線程資源。若是掛起後,也沒有發現任何能夠執行的代碼塊,一樣的也會進入阻塞狀態,這一點和線程是同樣的。code
在kotlin中,掛起函數只能在協程環境中使用。協程
等待一個協程執行完畢,和線程的API一致,使用join方法就能夠了。blog
val job = launch { // .... } job.join()
若是須要返回值,也可使用async來啓動協程,使用await方法來等待完成,並取得返回值數據。ip
val job = async { // .... } job.await()
await和join都是掛起函數。
協程應該被實現爲能夠被取消的,調用Job的cancel方法能夠取消。可是,若是咱們寫個while(true)的死循環怎麼取消呢?
顯然是取消不了的。
爲了能讓咱們的協程邏輯能被取消,就須要使用到協程的一個屬性isActive。
假設咱們有一個協程是下載一個文件,咱們想讓它能被取消。它多是這樣:
val dlJob = launch { var isFinished = false while (!isFinished) { // download ... if (dlSize == totalSize) { isFinished = true } } }
這樣的話,這個協程是沒法被取消的,它沒法被外側所操控,咱們可使用isActive來改寫一下。
val dlJob = launch { var isFinished = false while (!isFinished && isActive) { // 注意這裏 // download ... if (dlSize == totalSize) { isFinished = true } } }
只須要這樣,就能夠實現取消邏輯了。
問題也就隨之而來,像打開網絡鏈接,讀寫文件,老是須要去執行一些close的邏輯纔是符合規範的,若是協程被取消,就直接退出了,要如何才能回收打開的資源呢?
能夠經過try{...}finally{...}進行回收資源,就像這樣:
val dlJob = launch { try { var isFinished = false while (!isFinished && isActive) { // 注意這裏 // download ... if (dlSize == totalSize) { isFinished = true } } } finally { // close something } }
當job被取消後,finally方法裏面依然會在最後被執行,能夠在這裏進行一些回收的操做。
若是咱們指望一個協程最多隻能執行多少時間,超過這個時間就要被取消的時候,就可使用超時邏輯,可使用withTimeout函數來實現。
runBlocking(Dispatchers.Default) { try { // 只容許協程執行最多500毫秒 val job = withTimeout(500) { try { println("working 1") delay(1000) println("working 2") } finally { println("finally, I will do something") } } println("job $job") // 沒法被執行到 } catch (e: Throwable) { println("out coroutine $e") } }
若是超時了,則會拋異常,而且,這個函數與runBlocking是同樣的,都會阻塞當前線程。上面的代碼中,協程外的print不會被執行到。
若是不想拋異常,可使用另外一個超時函數withTimeoutOrNull。
runBlocking(Dispatchers.Default) { try { // 只容許協程執行最多500毫秒 val job = withTimeoutOrNull(500) { try { println("working 1") delay(1000) println("working 2") } finally { println("finally, I will do something") } } println("job $job") // 能夠被執行到 } catch (e: Throwable) { println("out coroutine $e") } }
最終運行的結果是:
working 1 finally, I will do something job null
若是咱們指望協程的代碼在不一樣的線程中來回跳轉,可使用withContext來實現。(emmmmm,這是什麼場景的需求呢?)
newSingleThreadContext("Ctx1").use { ctx1 -> newSingleThreadContext("Ctx2").use { ctx2 -> runBlocking(ctx1) { log("Started in ctx1") withContext(ctx2) { log("Working in ctx2") } log("Back to ctx1") } } }
這裏直接照搬文檔中的示例代碼,最後輸出的結果爲:
[Ctx1 @coroutine#1] Started in ctx1 [Ctx2 @coroutine#1] Working in ctx2 [Ctx1 @coroutine#1] Back to ctx1
以上就是操控協程的各類方法了。
掛起函數是協程中定義的概念,只能在協程中使用,掛起的含義是非阻塞的暫停,調度器會尋找須要運行的協程放到線程中去執行,若是找不到任何須要執行的協程,纔會將線程阻塞。
協程是能夠被取消的,任何系統提供的掛起函數內部都有取消的邏輯,若是本身的協程想要能夠被取消,就必須經過isActive變量來編寫邏輯。
取消後的協程老是會執行finally代碼塊,能夠在這裏進行一些資源回收的操做。
若是但願控制協程的工做時長,可使用withTimeout來限制協程。
經過withContext函數來將邏輯切換到其餘的線程上去。
以前的表格,就能夠獲得進一步的擴展了
若是你喜歡這篇文章,歡迎點贊評論打賞
更多幹貨內容,歡迎關注個人公衆號:好奇碼農君