交纏模式的形成方式
兩個子系統最初以獨立模組的形式存在。隨著時間推移,每個子系統都在共享的上帝物件上累積欄位:全域設定結構、單例管理器或靜態類別。每一次新增在隔離狀態下看似正確,但耦合在小規模測試中卻難以察覺。
此問題固化的三種基質:
VLC 媒體播放器。 音訊、視訊與播放清單共享單一鎖,保護全域播放器狀態。跳轉至時間戳記的請求會取得鎖、修改播放位置並清空音訊緩衝區。等待同一把鎖的視訊子系統因此停滯,播放清單子系統也無法預取。結果是三個獨立子系統透過單一狀態物件序列化。效能代價為 O(N) 鎖競爭,其中 N 為子系統數量,且與操作延遲成正比。
Redis 事件迴圈。 AOF fsync(磁碟寫入)、複製(網路寫入)與命令執行(CPU)共用單執行緒事件迴圈。每個操作單獨來看都正確;一次緩慢的 fsync 會拖垮命令執行,而複製延遲在寫入負載下會持續累積。耦合點在於:不同延遲特性的操作共用同一個執行上下文。
LevelDB VersionSet。 寫入路徑(memtable flush)與背景壓縮共用 VersionSet 鎖。壓縮任務會持有鎖數十毫秒,導致寫入路徑停滯。兩者都是必要操作,耦合是結構性的,而非時間上的。
關鍵區別
Intertangle 是一種結構性耦合,而非時間問題。競爭條件是指兩個執行緒在沒有同步的情況下存取共享狀態,解決方式是加入 mutex。
Intertangle 是兩個子系統依設計共享狀態。加入 mutex 無法消除耦合,只會將存取序列化;子系統仍然共享狀態,瓶頸反而更嚴重。
在 VLC 的 Intertangle 中加入 mutex 會使情況更糟:音訊、視訊與播放清單都必須等待同一把鎖。結構性解法是讓每個子系統擁有各自的狀態:使用階段快照(phase snapshot),在階段邊界凍結共享狀態的快照,讓各子系統獨立讀取,最後再將寫入合併回去。
結構性 vs 時間性
判斷是否為 Intertangle 的關鍵診斷問題是:加入 mutex 會解決問題,還是讓問題更糟?
競爭條件:加入互斥鎖即可修正。正確的存取順序可消除資料損毀。
交纏(Intertangle):加入互斥鎖可序列化存取,但仍保留結構性耦合。各子系統仍共享狀態。在負載下,它們仍會互相阻塞。瓶頸依然存在。
如何找出交纏(Intertangle)
三種偵測訊號:
1. 子系統之間共享可變動欄位。 一個上帝物件(god-object)擁有多個欄位,這些欄位會被多個子系統讀取與寫入。若移除某個子系統對欄位的存取導致另一個子系統無法運作,則表示它們共享狀態。
2. 單一互斥鎖保護不相關的操作。 同一個鎖同時保護音訊刷新、視訊解碼以及播放清單擷取:這三個子系統具有不同的延遲特性,卻彼此互相等待。徵兆:不相關的操作卻使用相同的鎖名稱。
3. 增加負載時出現效能退化。 當操作 B 同時執行時,操作 A 的延遲增加,即使 A 與 B 表面上看似獨立。它們其實並非獨立:它們共享狀態。
階段快照修復法
階段快照模式:
# BEFORE: subsystems read and write shared state directly
class GameWorld:
position = {} # 共享的可變物件
velocity = {} # 共享的可變物件
def physics_tick(world):
for entity in world.entities:
world.position[entity] += world.velocity[entity] # 在迴圈中寫入共享狀態
# AFTER: 在階段開始前凍結快照;寫入會進入 next_state 緩衝區
def physics_tick(world):
snapshot = world.freeze() # 不可變檢視
next_state = {}
for entity in snapshot.entities:
next_state[entity] = snapshot.position[entity] + snapshot.velocity[entity]
world.merge(next_state) # 在階段邊界進行原子合併
每個子系統都讀取快照。沒有子系統對其進行寫入。寫入操作會累積在緩衝區中,並在階段邊界以原子方式合併。此時各子系統可獨立執行:無鎖競爭、無順序依賴、無隱藏耦合。
套用修正
某團隊回報一項缺陷:其遊戲引擎的動畫系統與碰撞系統皆會寫入同一個共享實體變換物件。當兩者在同一 tick 內執行時,碰撞結果會取決於動畫是否先執行。加入互斥鎖雖然解決了順序問題,但現在每當碰撞系統執行廣域階段掃描時,動畫就會停滯。