自旋鎖與忙碌等待
假設會議室有人在用,而你只需要它十秒。你可以回座位、請人在它空出來時呼叫你(這得來回各走一趟),或者乾脆站在門口,一遍又一遍地試門把直到它打開。對於極短的等待,站著試比較快。自旋鎖就是「站在門口」這個策略:一把鎖,在無法取得時讓執行緒緊密地繞圈、一次次重新測試這把鎖,直到它空出來,而不是去睡覺。
那個緊密的重測迴圈就是忙碌等待(或稱自旋):CPU 在燒週期卻什麼有用的事都沒做,只是一直查「空了沒?」。取得鎖是建在一個原子操作上的——通常是原子交換或比較並交換——好讓「測試並佔用」這把鎖的動作本身不會在兩個執行緒之間被打斷。普通的阻塞式互斥鎖則是請作業系統把等待的執行緒送去睡、稍後再喚醒,這在等待可能很久時才是對的選擇。自旋勝過阻塞,要在三件事都成立時:臨界區間極短、鎖很少被爭用,且「睡下去再喚醒」(一次上下文切換,要花掉數千個週期)會比為這短暫等待自旋還貴。
自旋鎖在作業系統核心內部到處都是,那裡上下文切換可能根本辦不到、或貴得離譜——例如在處理中斷時。在一般應用程式碼裡,它通常是錯誤的預設:如果持鎖者在你自旋時被搶佔,你就白白燒掉一整個時間片、什麼也沒換到;而在單核心上自旋鎖可能是災難性的(你霸著 CPU 自旋時,持鎖者根本沒機會跑去釋放鎖)。誠實的準則是:預設就拿阻塞式互斥鎖,只有在你能證明臨界區間極小且爭用很低時,才考慮自旋鎖。
最小的自旋鎖:取得時 while (atomic_exchange(&lock, 1) == 1) { /* 自旋 */ },釋放時 atomic_store(&lock, 0)。這個交換原子地寫入 1 並告訴你舊值,所以只有看到 0 的那個執行緒進入了。
忙等到原子交換回傳 0 為止;只有在等待極短時才自旋。
即使沒爭用,自旋也不是免費的:天真的自旋迴圈會猛敲核心間共享的那條快取列,所以真實的自旋鎖會加上 pause 提示、常常還會退避。在橫跨一次睡眠、一次系統呼叫或一段長計算時還持著自旋鎖,幾乎一定是錯誤。