在 Python 中,多線程和多進程是常用的並發編程方式。它們都可以提高程式的執行效率,但是它們的實現原理卻有很大的不同。本文將深入探討 Python 多線程和多進程的區別,並解釋為什麼多線程被稱為「假的多線程」。我也是在剛開始入門 python 的時候,處理資料的時候發現多線程的效率反而比單線程的低問題才進一步了解到這個的。
多線程和多進程的區別#
多線程和多進程都是並發編程的方式,但它們的實現方式卻有很大的不同。多進程是指在作業系統中同時運行多個進程,每個進程都有自己獨立的地址空間和系統資源。多線程是指在同一個進程中同時運行多個線程,每個線程共享同一個地址空間和系統資源。
具體來說,多進程是通過創建新的進程來實現並發的。每個進程都有自己獨立的地址空間和系統資源,進程之間通過 IPC(Inter-Process Communication)機制來進行通信。多線程則是通過創建新的線程來實現並發的。所有線程共享同一個地址空間和系統資源,線程之間通過共享內存和同步機制來進行通信。
由於多進程是在作業系統層面實現的,並且每個進程都有自己獨立的地址空間和系統資源,所以多進程具有更好的穩定性和安全性。但是創建和銷毀進程需要消耗較大的系統資源,因此多進程適用於 CPU 密集型的任務。而多線程則適用於 IO 密集型的任務,因為線程之間切換的代價比進程之間切換的代價要小得多。
為什麼多線程是假的多線程#
在 Python 中,由於 GIL(Global Interpreter Lock)機制的存在,多線程並不能真正地實現並發。GIL 是 Python 解釋器中的一個鎖,它保證同一時刻只有一個線程可以執行 Python 字節碼。這意味著在任何時候只有一個線程可以真正地執行 Python 程式碼,即使在多核 CPU 上也是如此。
由於 GIL 機制的存在,Python 中的多線程被稱為「假的多線程」。雖然多個線程可以同時存在於記憶體中,但是它們並不能真正地並發執行 Python 程式碼。因此,在 Python 中使用多線程並不能提高 CPU 密集型任務的執行效率。
但是,對於 IO 密集型任務來說,Python 中的多線程還是有優勢的。因為在 IO 密集型任務中,大部分時間都花費在等待 IO 操作完成上面。在這種情況下,GIL 機制並不會對程式的執行效率造成太大的影響。
多線程和多進程的開啟方式#
在 Python 中,開啟多線程或者多進程都有多種方式。下面分別介紹它們的優缺點。
多線程開啟方式#
1. 使用 threading 模組#
import threading
def worker():
print("I'm working")
t = threading.Thread(target=worker)
t.start()
使用 threading 模組創建新線程非常簡單,只需要創建一個 Thread 物件,並將要執行的函數作為參數傳入即可。但是由於 GIL 機制的存在,使用多線程並不能真正地實現並發執行。
2. 使用 concurrent.futures 模組#
from concurrent.futures import ThreadPoolExecutor
def worker():
print("I'm working")
with ThreadPoolExecutor() as executor:
executor.submit(worker)
使用 concurrent.futures 模組可以更方便地創建新線程,並且還可以使用 ThreadPoolExecutor 類來管理線程池。但是由於 GIL 機制的存在,使用多線程並不能真正地實現並發執行。
多進程開啟方式#
1. 使用 multiprocessing 模組#
import multiprocessing
def worker():
print("I'm working")
p = multiprocessing.Process(target=worker)
p.start()
使用 multiprocessing 模組創建新進程非常簡單,只需要創建一個 Process 物件,並將要執行的函數作為參數傳入即可。由於每個進程都有自己獨立的地址空間和系統資源,因此可以真正地實現並發執行。
2. 使用 concurrent.futures 模組#
from concurrent.futures import ProcessPoolExecutor
def worker():
print("I'm working")
with ProcessPoolExecutor() as executor:
executor.submit(worker)
使用 concurrent.futures 模組可以更方便地創建新進程,並且還可以使用 ProcessPoolExecutor 類來管理進程池。由於每個進程都有自己獨立的地址空間和系統資源,因此可以真正地實現並發執行。
代碼範例#
下面給出一個使用 multiprocessing 模組實現並行計算圓周率的例子:
import multiprocessing
import time
def calc_pi(digits):
start = time.time()
pi = 0
for k in range(digits):
pi += ((-1) ** k) / (2 * k + 1)
pi *= 4
end = time.time()
print(f"Digits: {digits}, Time: {end - start:.4f}s")
return pi
if __name__ == '__main__':
with multiprocessing.Pool() as pool:
digits_list = [100000, 200000, 300000, 400000, 500000]
results = pool.map(calc_pi, digits_list)
for digits, pi in zip(digits_list, results):
print(f"Digits: {digits}, Pi: {pi:.10f}")
在這個例子中,我們使用 multiprocessing.Pool 類創建了一個進程池,並使用 map () 方法對 digits_list 中的每個元素調用 calc_pi () 函數進行計算。由於每個元素都在不同的進程中進行計算,因此可以真正地實現並行計算圓周率。
總結#
本文深入探討了 Python 多線程和多進程的區別,並解釋了為什麼多線程被稱為「假的多線程」。同時,我們還介紹了各種開啟多線程或者多進程的方法,並給出了代碼範例。在實際開發中,應該根據任務類型選擇合適的並發編程方式,以提高程式的執行效率。
20230712 更新#
據今天的新聞說 Meta 公司承諾要花費三個工程師年來刪除 GIL,現在在等待 Python 社區接受 PEP703 的提案。source