行為導向併發(Behavior-oriented concurrency,簡稱 BOC)是一種適合 Python 的新型平行與併發程式設計範式。

在 BOC 程式中,資料共享方式是讓每個行為在特定時間內擁有該資料的唯一擁有權,從而不需要使用鎖來協調存取。對 Python 程式設計師來說,這帶來許多好處。行為以裝飾器函式實現,從程式設計師的角度看,這些函式就像一般函式一樣。重要的是,程式設計師的任務從解決併發資料存取問題,轉變為組織資料流經函式。這使得程式更易理解、維護與擴充,並因能有效排程行為於多個程序中執行,解鎖多核心效能。

BOC 已在多種語言中實作,包括作為研究語言 Verona 的基礎,現在也已在 Python 中實現。

以下示範一個模擬煎蛋的程式:

當只有一位廚師時,沒有問題。但若加入第二位廚師,便可能產生問題。以程式設計術語來說,若將此單執行緒程式改為併發,資源將被共享,必須處理競爭。畢竟兩位廚師不能同時使用同一把刀!也必須處理操作順序,確保材料先準備好再使用。以下表格重新框架此程式:

理想狀況下,我們希望廚師能平行工作,以縮短早餐準備時間,如下圖所示:

若用傳統執行緒與鎖來編寫,可能會是這樣:

突然間變得複雜許多!首先,我們必須明確分配工作給不同執行緒。當然有自動化方法(如生產者/消費者佇列),但為簡化起見,我們將工作最佳分配給兩位廚師。其次,需要取得與釋放鎖。對廚師1來說相對簡單,但廚師2煎蛋時,還得等待廚師1準備好其他材料。這需要在每種材料上使用條件變數,讓廚師1能通知狀態變化。完整範例可見 Github。雖然可行,但我們相信有更簡單直觀的寫法。

cown(concurrent-owned variable,併發擁有變數)確保同一時間只有一個執行緒能存取其內容。此限制在 Python 的全域直譯器鎖(GIL)層級強制執行,意即 cown 同時只能被一個直譯器存取。然而,cown 的參考可在多個程序間共享。底層利用 Python 的跨直譯器資料 API,安全地在直譯器邊界間移動資料。舉例來說,若有一個 cown 被兩個不同直譯器引用:

此時,cown 內資料以跨直譯器資料(XIData)形式存在。

當直譯器 A 呼叫 acquire 時,cown 變更擁有權,內容用來建立一個可正常操作的新物件。

若直譯器 B 嘗試在直譯器 A 擁有時取得 cown,將拋出例外。只有一個直譯器可擁有 cown 的時間擁有權。

直譯器 A 修改 cown 內資料後釋放它。

直譯器 A 釋放後,cown 可再次被取得。

行為(behavior)是一段程式碼,執行前需取得零個或多個 cown。使用 Python 的 boc 函式庫,透過 @when 裝飾器定義:

此處注意幾點。首先,裝飾器列出行為依賴的 cown。接著,函式宣告參數數量與裝飾器中 cown 數量相同。最後,需透過 value 屬性存取 cown 內的值。宣告後,行為會在其 cown 可用時自動排程至下一個可用工作執行緒執行。行為還有其他特性,例如行為可產生另一行為:

此例中,一行為產生另一行為報告洋蔥狀態,不會阻塞主行為執行。因為此報告不需讀寫 cown 狀態,可無依賴排程,並在有空工作執行緒時執行,但會在目前取得的 cown 釋放後執行。若行為拋出例外怎麼辦?

此處示範如何基於行為結果排程工作。此例用於處理例外,也可用於取得計算結果並結合其他計算結果排程後續工作。接下來將以煎蛋範例展示 cown 與行為的應用。

使用 BOC 煮飯

了解 cown 與行為概念後,來看看如何重寫煎蛋範例:

程式變得更簡潔:無需鎖定。每個資源包裝成 cown。食譜因為不可變,不需包裝成 cown,可安全使用且無需協調。第二,所有依賴資源的動作明確宣告所需資源(使用 @when 裝飾器)。行為宣告順序決定排程順序,意即所有材料行為會先於煎蛋行為執行。結果是程式清晰易讀易理解。完整範例可見 Github。更多 BOC 範例也在我們的 Github 範例區。

BOC 的關鍵技術之一是 Erlang 式的 send 與 selective receive。底層以 C 語言實作快速非阻塞併發佇列,對使用者開放,方便跨執行緒與子直譯器通訊。send 函式永遠非阻塞,包含 tag(類似信箱)與 contents(可跨直譯器安全傳送的物件,使用跨直譯器 API,失敗時回退 pickle)。例如:send("calculator", ("+", 5))。

每則訊息可由 receive 指令接收。selective receive 可指定願意接收的 tag,例如:

match 陳述式是處理 receive 結果的理想方式。除了簡單阻塞呼叫,receive 也接受逾時設定,逾時時回傳特殊 tag 訊息,例如:

after 回呼可指定逾時時 receive 的回傳值。

想了解更多 send 與 receive 用法,請參考計算機範例。

boc 函式庫內建 Matrix 類別:一個由 C 支援的雙精度浮點數密集 2D 矩陣。因底層資料不在 Python 堆中,Matrix 可放入 cown,並在直譯器間安全共享,無需複製。

Matrix 支援元素對元素運算(+、-、*、/)及矩陣乘法(@):

可使用整數、切片或(列,欄)元組索引:

因 Matrix 跨直譯器安全,能自然與 cown 及 @when 搭配使用:

此模式廣泛用於 boids 群聚模擬,數百代理同時更新位置與速度,跨多個直譯器併發執行。

noticeboard 是一個全域鍵值存儲,讓行為共享輕量且最終一致的狀態,無需專用 cown。適合發布設定、計數器或狀態標誌,供多數行為讀取,少數更新。

使用 notice_write 發布值,呼叫為 fire-and-forget,立即返回,值由專用 noticeboard 執行緒非同步應用:

在 @when 行為內,呼叫 noticeboard() 取得所有條目的唯讀快照,或用 notice_read 讀取單一鍵:

快照每行為擷取一次並快取,同一行為內多次呼叫 noticeboard() 或 notice_read 看到一致視圖。

需原子性讀-改-寫時,使用 notice_update。提供的函式在 noticeboard 執行緒執行,無交錯:

注意更新函式必須可序列化,lambda 與閉包不可用。請使用模組層級函式、運算子函式或 functools.partial。

因行為在真正平行的子直譯器上執行(Python 3.12+ 每個子直譯器有獨立 GIL),bocpy 隨工作執行緒增加幾乎線性提升吞吐量。下圖為 14 核心機器上內建環狀基準測試結果(16×16 矩陣負載,重複 3 次,每次測量 8 秒):

CPython 3.14 · 14 核心 / 28 執行緒 AMD · bocpy v0.5.0

虛線為理想線性擴展(工作執行緒數翻倍,吞吐量翻倍)。bocpy 緊貼此曲線,得益於無鎖工作竊取排程器與零複製 cown 交接。實際工作負載若負載更重或環數更多,效率更佳。

BOC:適用於 Python 的行為導向併發模型BOC:適用於 Python 的行為導向併發模型BOC:適用於 Python 的行為導向併發模型BOC:適用於 Python 的行為導向併發模型BOC:適用於 Python 的行為導向併發模型BOC:適用於 Python 的行為導向併發模型