iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0
Software Development

異步程式設計修煉:Kotlin Coroutines 完全指南系列 第 15 篇

Day 14:withContext 切換執行緒,你以為的暫停其實是搬家

  • 分享至 

  • xImage
  •  

Day 13:Coroutine Context,協程隨身攜帶的那包設定 結尾留下一個問題:協程如果已經在執行了,執行到一半才發現接下來這段程式碼需要換一個 Dispatcher 執行,難道只能重新啟動一個新的子協程,把原本那段邏輯拆開嗎,還是有辦法在同一個協程內部直接切換,不需要多開一個子協程來完成這件事。

今天要正式回答這個問題。

執行到一半換個環境繼續跑

答案是可以,而且不需要拆出新的子協程。負責做這件事的,是一個叫做 withContext 的 suspend function。

withContext 做的事情很單純:暫時用一份新的 Context 取代目前正在使用的 Context,執行一段指定的程式碼,執行完之後再恢復回原本的 Context 繼續往下跑。整個過程從頭到尾都待在同一個協程裡,身份沒有變過,只是中途借用了另一份設定跑完一小段流程,跑完就把原本那份設定接回來。

withContext 本身也是一次暫停,只是目的不同

Day 02:第一個 suspend function,暫停到底暫停了什麼 建立過一組核心對照:暫停跟阻塞是兩件不同的事,阻塞讓執行緒被迫閒置死等,暫停則讓執行緒可以先去忙別的事,時機到了再回來接續。

當時舉的例子都是在等待某個外部結果,例如等網路回應。withContext 同樣會讓協程暫停,這件事沒有例外,但這次暫停要等的不是任何外部結果,協程本身也沒有真的停下來空等什麼事情發生。它暫停的目的,是把接下來要執行的程式碼搬到另一個 Dispatcher 指定的執行環境下恢復執行。

這正是本篇標題想點出的畫面。協程暫停之後,接下來的程式碼有可能改到另一條符合新 Dispatcher 要求的執行緒上恢復,不一定會回到原本那條執行緒,就像人先打包好行李,暫時搬到另一個地方把事情辦完,辦完再搬回原本的住處,繼續過原本的生活。對呼叫端而言,這整段搬家的過程是同步接續的,程式碼讀起來依然是由上而下依序執行,沒有回呼,也沒有額外的協程管理負擔,讀程式的人幾乎感受不到中間發生過搬家這件事。

用示範情境看 withContext 實際怎麼用

回到一路延續的多來源圖片下載與聚合工具情境。一個子協程原本在 Dispatchers.IO 之下執行下載工作,這是 Day 12:Dispatchers,協程實際跑在哪條執行緒上 已經確定的判斷,等待網路回應這種操作本來就該交給 IO。假設下載完成之後,緊接著要對這份圖片資料做一段格式轉換之類的運算密集處理,這時候可以用 withContext 切換到 Dispatchers.Default 執行這段運算,執行完之後再自然恢復回原本的 IO 環境繼續後續流程。

語法極簡,withContext 的回傳值就是那段程式碼執行完的結果,呼叫端可以直接把它當成一般函式呼叫般接收結果並繼續往下寫:

suspend fun downloadAndProcess(url: String): ProcessedImage {
    val rawData = downloadImage(url) // 目前在 Dispatchers.IO 下執行

    val processed = withContext(Dispatchers.Default) {
        processImageData(rawData) // 暫時搬到 Dispatchers.Default 執行
    }

    return processed // 回到原本的 Dispatchers.IO 繼續往下跑
}

processed 拿到的就是 processImageData 跑完的結果,接下來要怎麼用它,跟寫一般同步程式碼沒有任何差別。這裡不需要額外處理協程間的通訊,也不需要等待某個 Deferred,withContext 已經把整段搬家的過程包好了。

值得對照一下,如果錯誤地為了切換執行緒而改用重新啟動一個子協程來做同一件事,需要額外處理這個新子協程的父子關係,還要想辦法把運算結果收回來給原本的流程繼續使用。相較之下,withContext 直接對應「同一段邏輯只是換個環境執行」這個需求,不用多開一個身份、也不用多處理一層結果回收,這裡先點出差異,子協程實際的寫法留待有需要時再展開。

別忘了,覆寫的規則 Day 13 已經講過

withContext 看起來像是一個獨立的新行為,但它其實是 Day 13 建立的規則在執行期間的具體體現,並不是另外一套機制。

Day 13 講過,Coroutine Context 的元素可以組合,同類元素後蓋前。withContext 傳入的那份新 Context,正是拿去和目前協程手上的 Context 做一次組合:其中的 Dispatcher 元素覆蓋了原本的設定,其餘沒有特別指定的元素,例如代表生命週期的 Job,則沿用原本的值。

上面的範例裡,withContext(Dispatchers.Default) 只換掉了 Dispatcher 這一項,協程的身份、它在整棵父子結構裡的位置,全部維持不變,這也是為什麼整段搬家的過程完全不需要重新處理父子關係。

理解了這一層,withContext 就不再是一個要單獨死記的新語法,而是把 Day 13 抽象的組合覆寫規則,套用到執行中途的一次具體操作而已。

同一個身份切換環境,不需要多開一個協程

今天知道了 withContext 如何讓協程在同一個身份下切換執行環境,這是 Day 12:Dispatchers,協程實際跑在哪條執行緒上 與 Day 13:Coroutine Context,協程隨身攜帶的那包設定 兩個錨點在實際操作上的具體應用,執行緒切換不需要啟動新的協程,只是一次目的不同的暫停,一次搬家。

目前為止談的都還是協程如何正常執行、如何切換環境,還沒有觸及協程如果執行失敗了,會發生什麼事。這是接下來要面對的話題。


上一篇
Day 13:Coroutine Context,協程隨身攜帶的那包設定
系列文
異步程式設計修煉:Kotlin Coroutines 完全指南 共 15 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言