2013-02-14 49 views
8

我剛纔看到香草薩特的談話:C++ and Beyond 2012: Herb Sutter - atomic<> Weapons, 2 of 2混淆shared_ptr的析構函數中執行錯誤

他表示在執行的std :: shared_ptr的析構函數的錯誤:

if(control_block_ptr->refs.fetch_sub(1, memory_order_relaxed) == 0) 
    delete control_block_ptr; // B 

他說,這是由於memory_order_relaxed,刪除可以放在fetch_sub之前。

在1時25分18秒 - 發行不守B線下方,它應該是

這怎麼可能?發生之前/之前有序的關係,因爲它們都在單線程中。我可能是錯的,但在fetch_sub和delete之間也存在carry-a-dependency。

如果他是正確的,這ISO項目支持?

回答

0

在談話香草顯示memory_order_releasememory_order_relaxed,可輕鬆將有更多的問題。

除非delete control_block_ptr訪問control_block_ptr->refs(它可能沒有),那麼原子操作不會帶有刪除的依賴關係。刪除操作可能不會觸及控制塊中的任何內存,它可能只是將該指針返回給freestore分配器。

但我不知道,如果赫伯在談論編譯移動刪除該原子操作之前,或者只是當提到副作用成爲其他線程可見。

+0

「談論編譯器在原子操作之前移動刪除」 - 1:23:34:「代碼停留在下面和上面」;;;; 「或者只是指副作用何時對其他線程可見。」 - 哪些副作用?讀取 - 修改 - 寫入每次查看修改順序中的最後一個值 – qble 2013-02-14 19:04:10

+0

「但放鬆會帶來更多問題。」 - 哪些問題? – qble 2013-02-14 19:04:48

+0

_「哪些問題?」放鬆的操作根本不是同步操作。 – 2013-02-14 19:06:33

0

看起來他正在談論的未對他的代碼塊中所示(並且作爲結果 - 混淆)共享對象本身的動作,同步。

這就是爲什麼他把acq_rel - 因爲對象的所有操作應在其破壞之前發生,一切爲了。

但我仍不確定他爲什麼會談到deletefetch_sub交換。

+0

是的,需要獲取釋放操作的主要原因是線程間同步,他只是提到了代碼運動的傳遞,並沒有解釋它 – 2013-02-14 19:40:47

+0

「需要進行線程間同步」 - 需要代碼未顯示在幻燈片中... – qble 2013-02-14 19:43:28

+0

是的,僅顯示其中的一部分:控制塊可能包含自定義刪除程序,並且刪除程序可能會在不同的線程,所以控制塊一定不能被銷燬,直到刪除者完成銷燬對象。 – 2013-02-14 19:53:25

2

試想一下,釋放共享指針代碼:

auto tmp = &(the_ptr->a); 
*tmp = 10; 
the_ptr.dec_ref(); 

如果dec_ref()不具有「釋放」的語義,它是一個編譯器(或CPU)從之前dec_ref搬東西完全沒有問題()後它(例如):

auto tmp = &(the_ptr->a); 
the_ptr.dec_ref(); 
*tmp = 10; 

這是不是安全的,因爲dec_ref(),也可以從其他線程在同一時間打來電話,刪除對象。 因此,它必須在dec_ref()之前有一個「釋放」語義才能留在那裏。

現在,讓我們想象對象的析構函數是這樣的:

~object() { 
    auto xxx = a; 
    printf("%i\n", xxx); 
} 

同時,我們會修改例如一個位,將有2個線程:

// thread 1 
auto tmp = &(the_ptr->a); 
*tmp = 10; 
the_ptr.dec_ref(); 

// thread 2 
the_ptr.dec_ref(); 

於是,「聚集的」代碼看起來例如:

// thread 1 
auto tmp = &(the_ptr->a); 
*tmp = 10; 
{ // the_ptr.dec_ref(); 
    if (0 == atomic_sub(...)) { 
     { //~object() 
      auto xxx = a; 
      printf("%i\n", xxx); 
     } 
    } 
} 

// thread 2 
{ // the_ptr.dec_ref(); 
    if (0 == atomic_sub(...)) { 
     { //~object() 
      auto xxx = a; 
      printf("%i\n", xxx); 
     } 
    } 
} 

但是,如果我們只有atomic_sub()的「釋放」語義,則此c ODE可以優化的方式:

// thread 2 
auto xxx = the_ptr->a; // "auto xxx = a;" from destructor moved here 
{ // the_ptr.dec_ref(); 
    if (0 == atomic_sub(...)) { 
     { //~object() 
      printf("%i\n", xxx); 
     } 
    } 
} 

但這樣一來,析構函數並不總是打印「一」的最後一個值(此代碼賽馬不免費的了)。這就是爲什麼我們還需要爲atomic_sub獲取語義(或者,嚴格來說,當計數器在遞減後變爲0時,我們需要一個獲取屏障)。

+0

非常好的例子。這只是一個有非平凡dtors的對象的問題,它必須與對象的可變狀態一起工作,對嗎?那麼對於'delete'的內存釋放副作用,一個'放鬆的'原子是完全安全的嗎? – tmyklebu 2015-02-10 01:43:29

+0

換句話說,「釋放」語義足夠用於具有瑣碎析構函數的對象。 「刪除」本身顯然不能作爲整體操作重新排序 - 只有「讀取」部分可以從其中移出。 「寫入」部分不能移動「向上」,因爲不允許推測性寫入,並且應首先檢查「如果」條件。 – 2015-02-10 02:33:48

+0

我得到的釋放對於微不足道的瑣事是足夠的,但放鬆也足夠了? – tmyklebu 2015-02-10 03:46:24

0

這是一個很晚的回覆。

讓我們先用這個簡單類型:

struct foo 
{ 
    ~foo() { std::cout << value; } 
    int value; 
}; 

我們將在一個shared_ptr使用這種類型,如下所示:

void runs_in_separate_thread(std::shared_ptr<foo> my_ptr) 
{ 
    my_ptr->value = 5; 
    my_ptr.reset(); 
} 

int main() 
{ 
    std::shared_ptr<foo> my_ptr(new foo); 
    std::async(std::launch::async, runs_in_separate_thread, my_ptr); 
    my_ptr.reset(); 
} 

兩個線程並行運行,既共享foo對象的所有權。

使用正確的shared_ptr實施 (即,一個與memory_order_acq_rel),此程序已定義的行爲。 該程序將打印的唯一值是5

由於執行不正確(使用memory_order_relaxed), 沒有這種保證。該行爲未定義,因爲引入了數據競賽 foo::value。只有在主線程中調用析構函數 時纔會發生此問題。使用寬鬆的內存順序,其他線程中的寫入 至foo::value可能不會傳播到主線程中的析構函數。 可以打印除5以外的值。

那麼什麼是數據競賽?好了,檢查了定義,並注意最後一顆子彈點:

當一個表達式的評估寫入到存儲位置,而另一個評價讀取或修改同一個存儲單元中,表達式衝突說。有兩個相互矛盾的評估程序有一個數據的比賽,除非任何

  • 兩個相互矛盾的評價是原子操作(見的std ::原子)
  • 衝突評價一個之前發生另一個(見的std :: memory_order )

在我們的程序中,一個線程會寫foo::value和一個線程將從foo::value讀 。這些應該是連續的;在讀取之前應始終發生寫入 至foo::value。直觀地說,它是有意義的,因爲它們應該是析構函數應該是發生在對象上的最後一個 事情。

memory_order_relaxed雖然不提供此類訂購保證,但需要memory_order_acq_rel