達成等待無關的協助技術(the helping technique)
等待無關的核心有個張力。無鎖演算法讓一個執行緒可以永遠重試,所以任何單一執行緒都可能永遠不完成。要保證「每個」執行緒都在有界步數內完成,你需要一種辦法,讓一個不斷輸掉競賽的執行緒仍然把它的操作做完。協助(helping)技術就是答案:你不再只做自己的工作、指望自己贏,而是在發現別的執行緒的工作做到一半時替它完成,使得沒有人會被丟下。
它這樣構成。一個執行緒在共享處宣告它打算做的操作——通常是發布一個描述符(descriptor),一份完整描述它想做什麼的小記錄(哪個槽、什麼值、走到哪一步)。現在任何路過、看見一個進行中描述符的執行緒,不會只是繞過它;它讀取該描述符,代替擁有者執行那個操作剩下的步驟,並標記為完成。因為描述符捕捉了整個操作,協助者即使是替別人做,也能正確完成它。你看過的 Michael-Scott 佇列裡就有這個的雛形:任何發現 tail 落後的執行緒,會在做自己的 enqueue 之前先推進它。完整的等待無關建構把這一點推廣,使得在有界步數內,要嘛你完成自己的操作,要嘛有人替你完成。
誠實的現實是:協助既使一般的等待無關演算法成為可能,也使它們昂貴而錯綜。每個操作都必須表達成一個陌生人能撿起並完成的已發布描述符,這替快速路徑加上記憶體流量與複雜度;而把協助協定做對——使兩個協助者不會把同一操作完成兩次、或破壞彼此——是出了名的精細。這正是等待無關程式碼保留給「真正需要硬性每執行緒進展上界」之罕見場合、而非處處使用的核心原因。
/* 一個執行緒發布描述符;任何協助者都能完成該操作。 */ struct Desc { int op; void *target; void *arg; atomic int state; }; void help(struct Desc *d) { /* 由擁有者「或」路過者執行 */ if (d->state == PENDING) { apply(d->target, d->arg); /* 替 d 做剩下的工作 */ CAS(&d->state, PENDING, DONE); } }
因為描述符完整描述了操作,陌生人也能正確完成它——於是沒有執行緒會被飢餓到永不完成。
協助是從無鎖通往等待無關的標準途徑,但它使每個操作都變成可發布的描述符,增加負擔與微妙的正確性陷阱(重複完成、協助者競爭)。只在真正需要硬性每執行緒上界時才用它。