要理解非同步程式為什麼非走不可,得先弄清楚作業系統如何安排程式執行。這一節先把行程(Process)與執行緒(Thread)兩個基礎概念整理清楚,之後談 Coroutine 時才會看出它「輕量」的對比點。

行程(Process)#

行程是作業系統用來管理程式執行的最小單位之一,每一個應用程式(Application)通常對應一個行程。它具備幾個關鍵特性:

  • 擁有獨立的記憶體區段:程式碼、堆積、堆疊、靜態資料各有自己的空間。
  • 與其他行程隔離:A 行程不能直接讀寫 B 行程的記憶體,要溝通得透過 IPC(行程間通訊)機制。
  • 由作業系統排程:誰能用 CPU、用多久,由 OS 的排程器決定。

行程的隔離性是穩定性與安全性的基礎,但也讓行程之間的切換成本較高,因為 OS 必須儲存與還原大量的上下文。

執行緒(Thread)#

執行緒是行程裡實際在 CPU 上執行的單位。一個行程可以開出多條執行緒,預設情況下會有一條主執行緒(Main Thread)。

執行緒的特性:

  • 共享行程的記憶體:堆積、靜態資料是共享的,所以不同執行緒能看到彼此寫入的物件。
  • 各自擁有呼叫堆疊(Call Stack)與程式計數器:函式呼叫的歷史是各自獨立的。
  • 也由作業系統排程:執行緒之間切換時,OS 同樣要儲存暫存器、堆疊指標等上下文,再還原到下一條執行緒。

行程 vs 執行緒#

比較項行程(Process)執行緒(Thread)
記憶體獨立,不互通共用所屬行程的記憶體
隔離性高,預設不共享狀態低,需要自行做同步
建立成本高,需要分配獨立記憶體低於行程,但仍由 OS 管理
切換成本
排程主體作業系統作業系統

為什麼需要多執行緒#

回到前一節的情境:當主執行緒撞到耗時的 I/O 工作,畫面就會卡住。常見的解法是把耗時工作放到另一條執行緒去跑,讓主執行緒繼續處理畫面更新與輸入事件。

不過多執行緒不是萬靈丹:

  • 共享記憶體要小心:兩條執行緒同時寫一個變數,容易出現競態(Race Condition)。
  • 同步原語有成本:互斥鎖、條件變數、記憶體屏障都會付出延遲與複雜度。
  • 執行緒不是無限便宜:每條執行緒都要有自己的堆疊與排程結構,建立、切換、銷毀都要花錢。

「把工作丟去別的執行緒」聽起來很直覺,但執行緒數一多,效能會被切換成本與鎖競爭吃掉。後續會看到 Coroutine 透過協同式多工避開了這層成本。

多執行緒帶來的問題預告#

下面這幾個問題,後面的章節會逐一展開:

  • 寫法上:純粹的執行緒 + 回呼(Callback)會掉進回呼地獄(Callback Hell),可讀性與可維護性都差。
  • 控制上:取消、中斷、逾時這些需求,在原生執行緒上不容易做得乾淨。
  • 資源上:執行緒不便宜,動輒幾十萬條的場景很容易把系統打垮。

小結#

  • 行程是 OS 管理的隔離執行容器,執行緒是行程裡實際被排程到 CPU 的單位。
  • 多執行緒讓我們可以把耗時工作搬離主執行緒,但要付出共享記憶體與排程成本。
  • 接下來我們會檢視 JVM/Kotlin 上常見的執行緒函式庫,並指出純執行緒寫法的痛點。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10259764
  • 書本:Kotlin 小宇宙(iThome 鐵人賽系列書)