Python 執行緒(Threading)


在多工處理(Multitasking)的現代軟體開發中,「執行緒」是提升程式回應效率的關鍵技術。本文將從作業系統底層架構出發,逐步過渡到 Python 的實作層面。

一、 核心觀念:程序 vs 執行緒與系統資源的關係

在進入程式碼之前,必須先理清作業系統(OS)如何管理執行中的軟體。

1. 什麼是程序 (Process)?

  • 定義:在作業系統中,一個「執行中的程式」就被稱為一個程序。例如當你在 Windows 開啟 Google Chrome 或 Line,系統就會在主記憶體(RAM)中為它們分派獨立、隔離的記憶體空間。

  • 特性:程序之間是各自獨立的,一個程序崩潰(如 Chrome 某個分頁當掉)通常不會直接導致另一個程序(如 Line)當掉。

2. 什麼是執行緒 (Thread)?

  • 定義:執行緒是附屬於程序之下的微型執行單位,它是 CPU 進行工作排程(Scheduling)的最小核心單元。

  • 特性:同一個程序內的多個執行緒會共享該程序的記憶體空間(RAM)。例如一個 Google Chrome 程序內部可能同時跑了 20 個執行緒,分別處理網頁渲染、網路下載、JavaScript 解析等。

3. CPU Core、RAM、Process、Thread 的階層關係

當現代 CPU 擁有複數核心(Multi-core)時,作業系統會將程序分配給特定的核心去執行。

  • 核心 1 (Core 1) $\rightarrow$ 程序 1 (Process 1) 在獨立的 RAM 1 內運行 $\rightarrow$ 內部切分出 執行緒 A、執行緒 B

  • 同一個程序內的執行緒(Thread A 與 B)雖然看起來像在「同時」運作,但若是使用 Python,受限於其特殊的 GIL(全域解釋器鎖, Global Interpreter Lock) 機制,在同一個時間點,實際上只有一個執行緒能在同一個核心內被真正執行。作業系統是透過極為快速的「上下文切換(Context Switching)」,才讓人類體感上覺得它們在並行運作。


在Windows系統裡,可以從工作管理員(taskmgr)看到程序(process):

而這些程序是在作業系統裡執行中的程式,且在ram上有自己的記憶體空間,從上圖可以知道電腦裡有很多的程序在同一時間運行。而執行緒(thread)是附屬於程序的,一個程序可以擁有很多個執行緒,例如上圖中的Google Chrome可能有20個執行緒在運行中。現在的CPU有多個核心(Core)數,每一個程序會被作業系統分配到各自的 CPU Core裡。CPU Core、RAM、Process、Thread的關係如下圖:
以上圖中的 Process 1 為例,它被分配到 Core 1,有 RAM 1的記憶體空間,擁有 Thread A, B兩個執行緒。從上圖來看,執行緒好像可以同時運行,但,它只是看起來很像在同時執行,除此之外還有些其他議題需要考慮的。

二、 實作:Python 執行緒的建立與執行(底層 _thread 模組)

Python 提供兩種方式來操作執行緒:低階的 _thread(舊稱 thread)與高階的 threading 模組。本文採用了底層的 _thread 進行示範:

1. 核心函數語法

Python
_thread.start_new_thread( function, args[, kwargs] )

這個函數會立即配置一個新的執行緒,並在該執行緒中呼叫傳入的 function

  • function:執行緒要執行的函數名稱。

  • args:傳遞給函數的參數,必須是元組 (Tuple) 格式。

2. 範例程式碼逐行拆解

Python

import _thread
import time

t = 0

# 1. 定義子執行緒要執行的任務函數
def prt_time(threadID, interval):
    cnt = 0
    while cnt < 3:
        time.sleep(interval) # 讓該執行緒暫停(睡覺)指定秒數
        cnt += 1
        # 輸出當前執行緒的 ID 與系統時間
        print("%s 現在時間:%s" % (threadID, time.ctime(time.time())))

# 2. 嘗試建立並啟動兩個子執行緒
try:
    # 建立執行緒 A:每隔 1 秒印出一次時間,共印 3 次
    _thread.start_new_thread(prt_time, ("執行緒A", 1, )) 
    # 建立執行緒 B:每隔 3 秒印出一次時間,共印 3 次
    _thread.start_new_thread(prt_time, ("執行緒B", 3, )) 
except:
    print("錯誤: 執行緒無法執行")

# 3. 主執行緒(Main Thread)的生命週期維護
while t <= 10:
    time.sleep(1.5) # 主執行緒每隔 1.5 秒醒來一次
    print("主執行緒現在時間:%s" % (time.ctime(time.time())))
    t = t + 1

print("主執行緒結束")

🔍 關鍵邏輯與細節說明

  1. Tuple 參數的陷阱:在 ("執行緒A", 1, ) 中,最後面的逗號 , 絕對不能省略。因為在 Python 中,單一元素的括號 ("執行緒A") 只會被當成字串,加上逗號才會被正確識別為 Tuple。

  2. 為什麼主執行緒需要 while t <= 10 控制? 在作業系統的架構中,一旦「主執行緒(Main Thread)」執行結束,整個程序(Process)就會被系統強制關閉,此時附屬在底下的所有子執行緒不論有沒有跑完都會被瞬間掐斷。因此,程式末端設計了一個跑 10 次、每次睡 1.5 秒的迴圈(共維持約 15 秒),目的是「確保主執行緒活得夠久,讓子執行緒 A 與 B 有足夠的時間把任務執行完畢」。

範例程式結果:



三、 技術補充:給小編的擴充建議(進階的 threading 模組)

引入 Python 現代標準庫所推薦的 threading 模組。相較於原有的 _thread,高階的 threading 提供了更好維護的物件導向設計,以及解決主執行緒過早結束的 join() 機制:

💡 擴充範例(更優雅的寫法):

充範例(更優雅的寫法):

Python
import threading
import time

def prt_time(name, delay):
    for i in range(3):
        time.sleep(delay)
        print(f"{name} 現在時間: {time.ctime()}")

# 使用 threading.Thread 建立物件
threadA = threading.Thread(target=prt_time, args=("執行緒A", 1))
threadB = threading.Thread(target=prt_time, args=("執行緒B", 3))

# 啟動執行緒
threadA.start()
threadB.start()

# 關鍵:使用 join() 讓主執行緒「原地等待」子執行緒完成,不需要再寫死 while 迴圈!
threadA.join()
threadB.join()

print("主執行緒在子執行緒都結束後,才安全地劃下句點。")
範例程式結果:

四、執行緒同步化

有時執行緒必須等待另一個執行緒結束才能執行。這時可以使用 lock 的機制。

範例程式:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import threading
import time

t = 0
lock = threading.Lock()

class myThread (threading.Thread):
   def __init__(self, threadID, delay):
      threading.Thread.__init__(self)
      self.id = threadID
      self.delay = delay
      
   def run(self):
      print ("執行 " + self.id)
      lock.acquire()
      prt_time(self.id, self.delay)
      lock.release()
      print ("離開 " + self.id)


# 執行緒所用的輸出系統時間之函數
def prt_time( threadID, delay):
   cnt = 0
   while cnt < 3:
      time.sleep(delay)
      cnt += 1
      print( "%s 現在時間:%s" % ( threadID, time.ctime(time.time()) ) )

# 建立兩個執行緒
thread1 = myThread("執行緒A", 1)
thread2 = myThread("執行緒B", 3)

# 啟動執行緒
thread1.start()
thread2.start()

# 等待執行緒結束
thread1.join()
thread2.join()

print ("離開主執行緒")

範例程式結果:

以上介紹了 Python Threading 的基本用法,以及同步的作法。此外同步化有個經典的範例,有興趣的讀者可參考這篇文章:「Producer-Consumer Threading」。

📢小編碎碎念

多執行緒在處理 I/O 密集型工作(如網路爬蟲、檔案讀寫)時能大幅縮短等待時間!大家在寫 Python 多執行緒時,有遇過資料搶奪(Race Condition)的問題嗎?

若您覺得文章寫得不錯,請點選文章上的廣告,來支持小編,謝謝。

If you like this post, please click the ads on the blog or buy me a coffee. Thank you very much.

留言

這個網誌中的熱門文章