一、多執行緒
在CPU不密集、IO密集的任務下,多執行緒可以一定程度的提升執行效率。
import threading
import time
import requests
def fetch_url(url: str)->None:
'''根據地址發起請求,獲取響應
- url: 請求地址'''
response = requests.get(url)
print(f"{url}: {response.status_code}")
def fetch_urls_sequential(urls:list)->None:
start_time = time.time()
for url in urls:
fetch_url(url)
end_time = time.time()
print(f"使用單執行緒時間為: {end_time - start_time} 秒\n")
def fetch_urls_concurrent(urls:list)->None:
start_time = time.time()
threads = []
for url in urls:
thread = threading.Thread(target=fetch_url, args=(url,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
end_time = time.time()
print(f"使用多執行緒時間為: {end_time - start_time} 秒")
if __name__ == "__main__":
urls = ["http://www.example.com"]*10
fetch_urls_sequential(urls)
fetch_urls_concurrent(urls)
http://www.example.com: 200
http://www.example.com: 200
http://www.example.com: 200
http://www.example.com: 200
http://www.example.com: 200
http://www.example.com: 200
http://www.example.com: 200
http://www.example.com: 200
http://www.example.com: 200
http://www.example.com: 200
使用單執行緒時間為: 10.178432703018188 秒
http://www.example.com: 200
http://www.example.com: 200
http://www.example.com: 200
http://www.example.com: 200
http://www.example.com: 200
http://www.example.com: 200
http://www.example.com: 200
http://www.example.com: 200
http://www.example.com: 200
http://www.example.com: 200
使用多執行緒時間為: 0.5794060230255127 秒
可以看到在IO密集型任務時,排除極端情況,使用多執行緒可以很大的提升程式的效能。例如在這個例子中,響應時間就相差了8倍多。
雖然在Python中有GIL保護機制,但是依然需要注意執行緒安全。例如(共享資料、共享裝置、非原子性操作等)。可以使用鎖機制、訊號機制、佇列、管道等等。
二、協程
協程也叫輕量級執行緒,協程是一種在單一執行緒內實現併發程式設計的技術。它們允許函式在執行過程中暫停,並在稍後恢復,從而使得多個任務能夠交替執行,而不需要多個作業系統執行緒的開銷。協程透過讓出控制權來暫停執行,等待其他協程執行,然後在適當的時候恢復執行。
區別 | 執行緒 | 協程 |
---|---|---|
上下文切換 | 執行緒上下文切換由作業系統決定,消耗更大 | 協程的上下文切換由使用者自己決定,消耗更小 |
併發 | 執行緒是搶佔式的,作業系統可以隨時中斷執行緒排程了一個執行緒 | 協程是協程式的,需要主動讓出控制權時,才會進行任務切換 |
開銷 | 建立執行緒和銷燬執行緒,造成很大的開銷 | 基於單執行緒的,並且協程是輕量級的,不會消耗大量資源。 |
協程的優勢在於能夠更高效地利用系統資源,在執行多個任務時能夠充分利用CPU的效能。相比之下,併發執行的多個協程可以在單個執行緒內非阻塞地交替執行,從而減少了執行緒切換和上下文切換的開銷,提高了整體的執行效率。
所以協程本身並不會直接提升單個任務的執行時間,但是,如果一個任務可以分解為多個步驟,並且這些步驟之間存在依賴關係,那麼使用協程來執行這些步驟會更快。因為在等待I/O操作或其他非同步操作完成時,協程可以讓出CPU控制權,允許其他協程繼續執行,從而最大程度地減少了等待時間。
例如從網站下載頁面內容,並且計算頁面內容。這就是單任務多步驟,這種情況就可以體現出協程的優勢(效能、執行時間都會提升)。
import asyncio
import threading
import aiohttp
import time
import requests
async def fetch_page(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
async def compute_length(url):
page = await fetch_page(url)
return len(page)
async def async_main():
urls = ["http://www.example.com"]*10
tasks = [compute_length(url) for url in urls]
results = await asyncio.gather(*tasks)
print("內容長度為:", results)
def thread_fetch_page(url):
response = requests.get(url)
return response.text
def thread_compute_length(url):
page = thread_fetch_page(url)
return len(page)
def thread_main():
urls = ["http://www.example.com"]*10
threads = []
results = []
start_time = time.time()
for url in urls:
thread = threading.Thread(target=lambda u: results.append(thread_compute_length(u)), args=(url,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
end_time = time.time()
print("內容長度為:", results)
print(f"使用多執行緒時間為: {end_time - start_time} 秒")
if __name__ == "__main__":
start_time = time.time()
asyncio.run(async_main())
end_time = time.time()
print(f"使用協程時間為: {end_time - start_time} 秒")
thread_main()
內容長度為: [1256, 1256, 1256, 1256, 1256, 1256, 1256, 1256, 1256, 1256]
使用協程時間為: 0.5775842666625977 秒
內容長度為: [1256, 1256, 1256, 1256, 1256, 1256, 1256, 1256, 1256, 1256]
使用多執行緒時間為: 5.595600128173828 秒
可以看到這裡的協程的執行時間提升了很多,因為是單任務多步驟,類似於流水線的方式,所以協程的速度會快很多。並且這裡使用協程是單執行緒的,開銷更小;而多執行緒這裡使用了10個執行緒,開銷更大。
三、多程序
如果任務主要由 CPU 運算組成(CPU密集型任務),而不涉及太多的 I/O 操作,那麼多程序通常比多執行緒更適合,因為多程序能夠利用多核處理器的全部效能,每個程序獨立執行在自己的地址空間中,避開了 GIL 的限制。
例如大規模的計算,這種耗時的計算也是CPU密集型任務,使用多程序能明顯的提升效能。
這裡舉例計算大規模積分
import multiprocessing
import threading
import numpy as np
import time
def integrate(f, a, b, N):
"""使用梯形法則計算f在區間[a, b]上的積分,N為分割數"""
x = np.linspace(a, b, N)
y = f(x)
dx = (b - a) / (N - 1)
return np.trapz(y, dx=dx)
def f(x):
'''計算積分'''
return np.sin(x) * np.exp(-x)
def integrate_range(start, end, result, index):
result[index] = integrate(f, start, end, 100000000)
def thread_main():
result = [None] * 4
threads = []
ranges = [(0, 5), (5, 10), (10, 15), (15, 20)]
start_time = time.time()
for i, (start, end) in enumerate(ranges):
thread = threading.Thread(target=integrate_range, args=(start, end, result, i))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
end_time = time.time()
print(f"多執行緒使用時間為: {end_time - start_time} 秒")
print(f"積分結果: {result}\n")
def multiprocess_main():
manager = multiprocessing.Manager()
result = manager.list([None] * 4)
processes = []
ranges = [(0, 5), (5, 10), (10, 15), (15, 20)]
start_time = time.time()
for i, (start, end) in enumerate(ranges):
process = multiprocessing.Process(target=integrate_range, args=(start, end, result, i))
processes.append(process)
process.start()
for process in processes:
process.join()
end_time = time.time()
print(f"多程序使用時間為: {end_time - start_time} 秒")
print(f"積分結果: {result}")
if __name__ == "__main__":
thread_main()
multiprocess_main()
多執行緒使用時間為: 7.396134853363037 秒
積分結果: [0.5022749400837572, -0.0022435439294056455, -3.1379421486677834e-05, -1.809428816655182e-08]
多程序使用時間為: 4.97518515586853 秒
積分結果: [0.5022749400837572, -0.0022435439294056455, -3.1379421486677834e-05, -1.809428816655182e-08]
可以看到這裡的區別還是很大的,如果資料量更大,那麼程序的優勢會更明顯。因為如果計算的時間過快,那麼執行緒可以很快的進行切換。所以在大規模計算時,才可以體現出程序的優勢。
四、總結
特性 | 程序 | 執行緒 | 協程 |
---|---|---|---|
建立開銷 | 大 | 小 | 極小 |
切換開銷 | 大 | 小 | 極小 |
記憶體共享 | 不共享 | 共享 | 共享 |
通訊方式 | 管道、佇列等 | 共享記憶體 | 直接呼叫 |
多核利用 | 是 | 受GIL影響 | 否 |
使用場景 | CPU密集型任務 | IO密集型任務 | 高併發IO密集型任務 |
複雜度 | 較高 | 較低 | 依賴非同步程式設計。較高 |
1、程序(Process)
-
定義:程序是作業系統分配資源和排程的基本單位。每個程序擁有獨立的記憶體空間、檔案描述符和其他資源。
-
優點:
- 獨立性:程序之間相互獨立,不會直接影響彼此的執行,崩潰一個程序不會影響其他程序。
- 利用多核:能夠充分利用多核 CPU 的優勢,每個程序可以在不同的 CPU 核心上並行執行。
-
缺點:
- 開銷大:程序建立和銷燬的開銷較大,包括記憶體空間、檔案控制代碼等資源。
- 通訊複雜:程序間通訊(IPC)比較複雜,常用的 IPC 機制包括管道、訊息佇列、共享記憶體等。
-
使用場景:
- CPU 密集型任務,計算量大且需要充分利用多核 CPU 效能。
- 需要高可靠性的任務,程序隔離可以防止任務間相互影響。
2、執行緒(Thread)
-
定義:執行緒是程序中的一個執行流,是 CPU 排程和執行的基本單位。執行緒共享程序的記憶體和資源。
-
優點:
- 輕量級:建立和銷燬執行緒的開銷較小,執行緒之間的上下文切換開銷比程序小。
- 共享記憶體:同一程序的執行緒共享記憶體和資源,資料交換和通訊更方便。
-
缺點:
- GIL 限制:在 Python 中,由於全域性直譯器鎖(GIL),多執行緒在同一時間只能有一個執行緒執行 Python 位元組碼,限制了多執行緒在 CPU 密集型任務中的效能提升。
- 執行緒安全:共享資料時需要小心處理執行緒同步問題,避免資料競爭、死鎖等問題。
-
使用場景:
- I/O 密集型任務,如檔案讀寫、網路請求等,可以在等待 I/O 完成時切換執行緒,提升效率。
- 任務之間需要頻繁的資料共享和通訊的場景。
3、協程(Coroutine)
-
定義:協程是一種更輕量級的併發執行方式,協程在使用者空間內實現切換,由程式自身控制,不依賴作業系統的排程。
-
優點:
- 極輕量:協程的建立和切換開銷極小,適合大量併發任務。
- 無鎖程式設計:協程之間通常不需要鎖機制,因為協程在同一個執行緒中執行,不存在多執行緒的競爭問題。
- 高效利用 I/O 等待:協程特別適合 I/O 密集型任務,可以在 I/O 等待時切換到其他協程執行。
-
缺點:
- 單執行緒限制:協程在單執行緒中執行,無法利用多核 CPU 的優勢。
- 需要非同步程式設計支援:需要語言和框架的非同步支援,編寫非同步程式碼較為複雜。
-
使用場景:
- 高併發的 I/O 密集型任務,如大量的網路請求處理、Web 伺服器等。
- 需要大量併發但任務之間獨立性較高的場景。