文章目錄
- 無鎖隊列中的ABA問題
- ABA問題解決方案
ABA問題:CAS在操作的時候會檢查變量的值是否被更改過,如果沒有則更新值,但是帶來一個問題,最開始的值是A,接著變成B,最后又變成了A。經過檢查這個值確實沒有修改過,因為最后的值還是A,但是實際上這個值確實已經被修改過了。
聽起來似乎沒有什么嚴重的問題,舉幾個實際的例子看下。
無鎖隊列中的ABA問題
/* Naive lock-free stack which suffers from ABA problem.*/
class Stack {std::atomic<Obj*> top_ptr;//// Pops the top object and returns a pointer to it.//Obj* Pop() {while (1) {Obj* ret_ptr = top_ptr;if (ret_ptr == nullptr) return nullptr;// For simplicity, suppose that we can ensure that this dereference is safe// (i.e., that no other thread has popped the stack in the meantime).Obj* next_ptr = ret_ptr->next;// If the top node is still ret, then assume no one has changed the stack.// (That statement is not always true because of the ABA problem)// Atomically replace top with next.if (top_ptr.compare_exchange_weak(ret_ptr, next_ptr)) {return ret_ptr;}// The stack has changed, start over.}}//// Pushes the object specified by obj_ptr to stack.//void Push(Obj* obj_ptr) {while (1) {Obj* next_ptr = top_ptr;obj_ptr->next = next_ptr;// If the top node is still next, then assume no one has changed the stack.// (That statement is not always true because of the ABA problem)// Atomically replace top with obj.if (top_ptr.compare_exchange_weak(next_ptr, obj_ptr)) {return;}// The stack has changed, start over.}}
};
考慮有兩個線程并發的訪問該隊列。
初始時,棧頂元素是 A,A 指向 B,B 指向 C。
top --> A --> B --> C
- Thread 1 執行 pop 操作,將棧頂元素 A 彈出,取出了top_ptr, 和 next_ptr后被中斷。
Obj* ret_ptr = top_ptr;
if (ret_ptr == nullptr) return nullptr;// For simplicity, suppose that we can ensure that this dereference is safe// (i.e., that no other thread has popped the stack in the meantime).Obj* next_ptr = ret_ptr->next;
此刻,Thread 1里看到的是top_ptr等于A, next_ptr 等于B,問題其實就在這里了,保證top_ptr等于A時,并不能保證next_ptr等于B。
- Thread 2 執行 pop 操作,將棧頂元素從 A 改為 B。
top --> B --> C
- Thread 2 再次執行 pop 操作,將棧頂元素從 B 改為 C。
top --> C
- Thread 2 執行 push 操作,將元素 A 推回到棧頂。
top --> A --> C
- Thread 1 繼續執行,執行 compare_exchange_weak(A, B),由于 top == ret,操作成功,棧頂元素變為了 B。
此刻
top --> B(already delete)
- Thread 1 訪問棧頂元素,但是 B 已經被刪除,這導致了 ABA 問題。
當從列表中刪除一個項目并釋放其內存后,如果再次分配一個新項目并將其添加到列表中,由于最近最常使用(MRU)的內存分配策略,新分配的對象通常會位于被刪除對象的相同位置。
或者是在啟用緩存機制時,重新分配的對象也有極大的概率是之前釋放的對象。
ABA問題解決方案
在原有的內容上添加額外的“標簽”或“戳記”位。例如,使用比較和交換(compare and swap)操作指針的算法可以使用地址的低位來表示指針成功修改的次數。因此,即使地址相同,下一次比較和交換操作也會失敗,因為標簽位不匹配。這種情況有時被稱為ABA?,因為第二個A與第一個略有不同。這種帶標簽的狀態引用也被用于事務內存(transactional memory)中。
在實現中,可以使用帶標簽的指針來解決ABA問題,其中指針的低位用于存儲標簽信息。然而,如果支持雙寬比較和交換(double-width CAS),更常見的做法是使用單獨的標簽字段。
通過使用標簽位,每次對共享數據進行操作時都會更新標簽,即使地址相同,標簽的變化也能夠反映出對象的修改歷史。這樣,在進行比較和交換操作時,除了比較地址外,還需要比較標簽位,從而更可靠地檢測到對象的變化。
如果“標簽”字段發生了環繞(wraparound),那么對抗ABA問題的保證就不再有效。然而,根據觀察,在當前存在的CPU上,并且使用60位標簽,只要程序的生命周期(即在不重新啟動程序的情況下)限制在10年內,就不會發生環繞;此外,有人認為,為了實際目的,通常只需擁有40-48位的標簽來確保不會發生環繞。由于現代CPU(特別是所有現代x64 CPU)傾向于支持128位的CAS(比較和交換)操作,這可以提供對抗ABA問題的可靠保證。
當使用128位CAS操作時,除了存儲指針地址外,還可以存儲一個更大的標簽值。因為標簽位數更多,所以即使在長時間運行的程序中,標簽的環繞概率也非常低,幾乎可以忽略不計。
通過使用128位CAS操作,并保留足夠長度的標簽位,可以提供對抗ABA問題的堅實保證。這意味著在多線程或并發環境中,即使對象的地址沒有改變,只要標簽發生變化,CAS操作也會失敗,從而可以正確檢測到對象的變化。這種方法在實踐中被廣泛采用,以確保數據的一致性和并發操作的正確性。
所謂的版本號、標記基本都是采用這個原理。