你的 Python 系統有好幾個模組。一個在監控市場環境,一個在管理倉位大小,一個在追蹤策略表現。每個模組各做各的事,看起來乾淨俐落。
但它們共享一個你可能沒注意到的東西:跟交易平台的那一條連線。這條連線是 process-global 的——整個程式只有一條,所有模組輪流用。平常沒事。但在某個特定的時序下,它會在你最意想不到的時候爆炸。
一個真實的場景
假設你的系統長這樣:
模組 A 每五分鐘要連到交易平台,拉十個商品的最新報價。它的做法是依序切換到每個商品的資料頻道,拉資料,再切下一個。十個商品跑完大概要幾秒鐘。
模組 B 每一分鐘也要連到同一個交易平台,但它是去查帳戶的持倉資訊。查之前它會先把連線切到帳戶管理的頻道。
大多數時候兩個模組不會撞上。但如果模組 A 正好在拉第五個商品的資料,模組 B 這時候把連線切到帳戶管理的頻道——模組 A 繼續拉第六個商品的時候,它拿到的不是第六個商品的資料,而是帳戶管理頻道回傳的東西。
你的環境判斷模組拿到了錯誤的資料。 它以為市場在暴跌,其實只是拿到了帳戶餘額。
為什麼這個 bug 這麼難抓
這種 race condition 有三個讓人頭痛的特性:
第一,它是間歇性的。 99% 的時候兩個模組不會同時搶連線。所以你的系統大部分時間看起來完全正常。你可能跑了一個月都沒出事,然後某天凌晨三點交易量低的時候,timing 剛好撞上,資料就亂了。
第二,它在不同機器上表現不同。 如果你像很多人一樣跑了好幾台伺服器,可能兩台正常一台異常。因為每台機器的 CPU 排程、網路延遲、任務啟動時序都不一樣。同一份程式碼在不同機器上的 race condition 觸發機率不同。
第三,它不會直接 crash。 模組 A 拿到錯誤的資料後,不會報錯——因為它不知道這些資料是錯的。它只是會基於錯誤的資訊做出錯誤的判斷。你的環境判斷可能會誤判市場狀態,進而錯誤調整倉位大小。這種錯誤可能很小(少賺一點),也可能很大(在不該加倉的時候加倉)。
修復原則:batch-lock
解法的概念其實不複雜,就是確保每個模組在使用連線的時候,不會被其他模組打斷。
最直覺的做法是 per-item lock——每拉一個商品的資料就鎖一次、放一次。但這有兩個問題:鎖的開銷累積起來不小;而且如果你在拉十個商品的中間放鎖,另一個模組可能趁空檔切走連線,你下一個商品的資料還是會出問題。
更好的做法是 batch-lock:一個模組要拉十個商品的資料,就把整批操作鎖一次。鎖住 → 連到第一個商品 → 拉資料 → 連到第二個 → 拉資料 → …… → 十個都拉完 → 放鎖。
這樣保證了一個模組在做完所有 item 之前,連線不會被切走。其他模組等著就好——反正整批操作只要幾秒鐘。
# 概念示意(不是生產程式碼)
with connection_lock:
for symbol in symbols:
switch_to(symbol)
data[symbol] = fetch_data()
不只是交易平台
如果你覺得「我的系統不用這麼複雜的交易平台 API」所以沒這個問題——再想想。
同樣的 race condition 會出現在任何 process-global 的共享資源上:
資料庫連線池。 如果你的多個模組共享一個連線池,而某個模組跑了一個長查詢佔住連線,其他模組的查詢就會超時或拿到舊資料。
API session。 如果你的系統用同一個 HTTP session 呼叫不同的 API endpoint,session 的 header 或 auth token 可能被另一個模組改掉。
檔案系統。 如果兩個模組同時寫同一個 log 檔或 config 檔——這個大家都知道,但真的有人會忘。
全域變數。 Python 的全域變數沒有執行緒安全保護。如果你的模組用全域變數存狀態,恭喜你,你有了最經典的 race condition。
怎麼預防
第一步:列出所有共享資源。 不只是交易平台連線。資料庫、API、檔案、全域變數,全部列出來。
第二步:檢查每個共享資源有多少模組在用。 如果一個資源只有一個模組在用,不需要鎖。如果有兩個以上,需要保護。
第三步:選擇鎖的粒度。 用 batch-lock,不要用 per-item lock。確保每個模組完整做完它的事情才釋放鎖。
第四步:加入 logging。 在每次加鎖和放鎖的時候記錄時間戳。這樣如果出了問題,你可以回去看 log,找到兩個模組是不是在某個時間點搶過同一個鎖。
最重要的是:如果你的系統跑在多台機器上,不要因為「兩台正常」就假設沒問題。 那台異常的機器可能只是 race condition 觸發得比較頻繁而已。問題是相同的,只是機率不同。
這種 bug 不會在回測裡出現——因為回測是單執行緒的,不存在 race condition。它只會在 live 環境裡咬你一口。而且咬的時候,你不會知道。




















