13.並行、平行與非同步
• 學習目標
– 認識並行、平行與非同步
– 使用 threading 模組
– 使用 multiprocessing 模組
– 使用 concurrent.futures模組
– 運用 async、await 與 asyncio
2
並行
• 多個流程可以並行(Concurrency)處理,
也就是從使用者的觀點來看,會是同時
「執行」各個流程
• 然而實際上,是同時「管理」多個流程
3
簡介執行緒
4
5
• 雖可以繼承 threading.Thread, 在
__init__()呼叫 super().__init__(),
並在類別中定義run()方法來實作執行緒
• 不過是不建議的,因為這會使得你的流程與
threading.Thread 產生相依性
6
7
8
• python 直譯器同時間只允許執行一個執行
緒,因此並不是真正的平行(Parallel)處
理,只不過「有時候」切換速度快到人類
感覺上像是同時處理罷了
• 執行緒適用的場合之一,就是非計算密集
的場合,因為與其等待某個阻斷作業完成,
不如趁著等待的時間來進行其他執行緒
9
10
11
• 對於計算密集的任務,使用執行緒不見得
會提高處理效率,反而容易因為直譯器必
須切換執行緒而耗費不必要的成本,使得
效率變差。
12
• 如果主執行緒中啟動了額外執行緒,預設
會等待被啟動的所有執行緒都執行完才中
止程式。
• 如果一個 Thread 建立時,指定了daemon
參數為 True,在所有的非 Daemon 的執
行緒都結束時,程式就會直接終止
• 如果需要在背景執行一些常駐任務,就可
以指定 daemon 參數為 True。
13
• 當執行緒使用 join() 加入至另一執行緒
時,另一執行緒會等待被加入的執行緒工
作完畢,然後再繼續它的動作
14
• 如果要停止執行緒,必須自行實作,讓執
行緒跑完應有的流程
15
競速、鎖定、死結
• 如果執行緒之間不需要共享資料, 或者共
享的資料是不可變動(Immutable)的型
態,事情會單純一些
• 然而,執行緒之間經常得共用一些可變動
狀態的資料…
• 要是執行緒之間需要共享的是可變動狀態
的資料,就會有可能發生競速狀況…
16
17
• 若要避免競速的情況發生,就必須資源被
變更與取用時的關鍵程式碼進行鎖定
18
19
• threading.Lock 實作了情境管理器協定,
可以搭配 with 來簡化 acquire() 與
release() 的呼叫
20
21
• 執行緒無法取得鎖定時會造成阻斷,不正
確地使用 Lock 有可能造成效能低落,另一
問題則是死結
22
23
• threading.RLock 實現了可重入鎖
(Reentrant lock)
• 同一執行緒可以重複呼叫同一個
threading.RLock 實例的 acquire()
而不被阻斷
• release()時也要有對應於 acquire()
的次數,方可以完全解除鎖定
• threading.RLock 也實作了情境管理器
協定,可搭配 with 來使用
24
• 另一個經常使用的鎖定機制是
threading.Condition
• 某個執行緒在透過 acquire() 取得鎖定之
後,若需要在特定條件符合之前等待,可
以呼叫 wait() 方法,這會釋放鎖定
• 若其他執行緒的運作促成特定條件成立,
可以呼叫同一 threading.Condition 實
例的 notify(),通知等待條件的一個執
行緒可取得鎖定
25
• 若等待中的執行緒取得鎖定,就會從上次
呼叫 wait() 方法處繼續執行
• 如果等待中的執行緒有多個,還可以呼叫
notify_all(),這會通知全部等待中的
執行緒爭取鎖定
26
27
28
• 如果需要這種一進一出,在執行緒之間交
換資料的方式,Python 標準程式庫中提供
了 queue.Queue
29
• 建立Semaphore 可指定計數器初始值
• 每呼叫一次 acquire(),計數器值遞減一,
在計數器為0 時若呼叫了 acquire(),執
行緒就會被阻斷
• 每呼叫一次 release(),計數器值遞增一,
如果 release()前計數器為 0,而且有執
行緒正在等待,在 release() 並遞增計數
器之後,會通知等待中的執行緒
30
• 可以設定一個 Barrier 並指定數量
• 如果有執行緒先來到這個柵欄,它必須等
待其他執行緒也來到這個柵欄
• 指定的執行緒數量達到,全部執行緒才能
繼續往下執行
31
32
平行
• 針對計算密集式的運算,若能在一個新的
行程(Process)平行(Parallel)運行,在
今日電腦普遍都有多個核心的情況下,就
有機會跑得更快一些。
33
• subprocess 模組可以讓你在執行 Python
程式的過程中,產生新的子行程
34
• 從 Python 3.5 開始,建議使用 run() 函
式來呼叫子行程
• subprocess.run() 執行之後會傳回
CompletedProcess 實例
• 若想要能取得標準輸出的執行結果:
35
• 如果子行程必須接受標準輸入:
36
• subprocess.run() 的底層是透過
subprocess.Popen() 實作出來的
37
• subprocess.Popen() 執行程式,會立
即傳回 Popen 實例,不會等待子行程結束
38
39
• 如果想要以子行程來執行函式,然而使用
類似 threading 模組的 API 介面,那麼
可以使用 multiprocessing 模組
40
41
• 建議在使用 multiprocessing 模組時,
最好的方式是不要共享狀態
• 然而有時行程之間難免需要進行溝通,
multiprocessing.Queue 是執行緒與行
程安全的,實作了必要的鎖定機制
42
43
44
• multiprocessing.Lock 也實作了情境
管理器協定
45
46
非同步
• Python 3.2 新增 concurrent.futures
模組,它提供了執行緒或行程高階封裝,
也便於實現非同步的任務
• 從 Python 3.5 之後,提供了async、
await 等語法,以及 asyncio 模組的支援,
如果非同步任務涉及大量的輸入輸出,可
以善用這些特性
47
使用 concurrent.futures
• 提供了 ThreadPoolExecutor 與
ProcessPoolExecutor 等高階API,分
別為執行緒與行程提供了工作者池的服務
48
49
• 對於計算密集式的任務,可以使用
ProcessPoolExecutor
50
51
• 使用 map() 方法來簡化程式的撰寫
52
Future 與非同步
• 獨立於程式主流程的任務、事件生成,以
及處理事件的方式,稱為非同步
• 使用執行緒或行程時,若想實現非同步概
念,方式之一是採用註冊回呼函式
53
54
• executor 的submit() 執行過後會傳回
Future,擁有 add_done_callback()
55
• 在下載的同時實現簡單的進度列:
56
略談 yield from 與非同步
57
• 若後續處理為數個非同步函式的話,整個
流程會馬上陷入難以理解的狀態
• Python 3.3 時新增了 yield from 語法,
Python 3.4 的 asyncio 與某些第三方程式
庫,曾基於這個語法提出了解決方案
• Python 3.5 後建議不要使用 yield from,
建議使用 async、await
58
59
yield from 與 Future
60
• 有多個非同步函式,顯然就需要個迴圈:
61
• 程式執行雖然是非同步,然而撰寫風格上
卻像是循序
• 若有人想呼叫 asyncTasks() 呢?甚至是
在流程上組合多個這類的函式?
62
63
64
Asyncio與並行
• 並非一定要多執行緒或多行程,才能實現
並行
• 多執行緒或多行程只是實現並行比較容易
• 只要執行環境支援,在單一行程、單一執
行緒中,也有可能實現並行
– 在遇到阻斷操作時會讓出(yield)流程控制權
給呼叫函式者,那呼叫函式的一方,就可以繼
續下個並行任務的啟動
65
• 如果在定義函式時,加上了 async 關鍵字,
呼叫該函式並不會馬上執行函式流程,而
是傳回一個 coroutine 物件
• 想要執行函式中定義的流程,可以透過
asyncio.run() 函式
66
• async def定義的函式是個非同步任務
• 執行asyncio.run()會阻斷,直到該執行
緒完成指定的任務
• 如果有多個任務要指定呢?Python 3.7以
後可以使用asyncio.create_task()
67
• 必須在async def函式中呼叫
asyncio.create_task()
• 也就是說,若有多個要執行的非同步任務,
必須在async def的函式中組織
• asyncio.create_task()不會阻斷執行
緒
68
69
事件迴圈
• 大部份基於Asyncio的函式,建議使用
asyncio.run()來執行
• 在Python 3.7之前沒有
asyncio.create_task(),就是透過這種
方式來建立多個任務
70
• 方才第二個REPL範例,若要直接操作事件
迴圈
71
• 在Python 3.7以後,若不想傳遞事件迴圈
實例給async def函式,可以使用
asyncio.get_running_loop()
72
async、await 與非同步
• async 用來標示函式執行時是非同步,也
就是函式中定義定義了獨立於程式主流程
的任務
• 後續若要在任務完成時,做進一步的處理
也是可行的
• 從 Python 3.5 開始,可以使用await
73
74
• 就語義上,若想等待 async 函式執行完後,
再執行後續的流程,可以使用await
• async 函式的任務完成後若有傳回值,會
成為 await 的傳回值
75
76
77
非同步產生器與 async for
78
• 迭代產生器時會以阻斷方式讀取 URL 後傳
回 bytes
79
情境管理器與 async with
80
81

13.並行、平行與非同步