2016-09-16 1 views
4

В книге под названием «C++ параллелизм в действии» Энтони Уильямс, в разделе 7.2.1, реализация стека безблокировочного находится в списке:Зачем «удалять» узлы в этом стековом классе без блокировки может привести к состоянию гонки?

template <typename T> 
class lock_free_stack { 
    struct node { 
     shared_ptr<T> data_; 
     node* next_; 
     node(const T& data) : data_(make_shared(data)) {} 
    }; 
    atomic<node*> head_; 
public: 
    void push(const T& data) 
    { 
     node* new_node = new node(data); 
     new_node->next_ = head_.load(); 
     while(!head.compare_exchange_weak(new_node->next_, new_node)); 
    } 
    shared_ptr<T> pop() 
    { 
     node* old_head = head_.load(); 
     while (old_head && 
       !head_.compare_exchange_weak(old_head, head_->next_)); 
     return old_head ? old_head->data_ : shared_ptr<T>(); 
    } 
}; 

Затем в разделе 7.2.2, автор говорит». .. в pop() мы выбрали утечку узлов, чтобы избежать условия гонки, когда один поток удаляет узел, в то время как другой поток по-прежнему содержит указатель на него, что он вот-вот разыгрывает ».

1) Я не понимаю, почему такой сценарий может произойти и почему функция следующего попа() может вызвать состояние гонки:

shared_ptr<T> pop() 
{ 
    node* old_head = head_.load(); // (1) 
    while (old_head && 
      !head_.compare_exchange_weak(old_head, head_->next_)); // (2) 
    shared_ptr<T> res; // (3) 
    if (old_head) { 
     res.swap(old_head->data); 
     delete old_head; 
     return res; 
    } else { 
     return {}; 
    } 
} 

2) Как приходит, что для нескольких потоков, которые называют поп() в то же время переменная «old_head» может указывать на тот же объект объекта после строки (3)?

ответ

8

Резьба 1 переходит к (2). Он начинает оценивать head_->next. Он загружает head_ в регистр, а затем отдает приоритет.

Резьба 2 исходит от начала до конца функции. Он удаляет head_ из существования, удаляя его и возвращает содержимое head_.

Нить 1 просыпается. Он следует head_ в регистре, получающем поле ->next. Но нить 2 уже удалила данные, на которые указывает head_, и мы просто следовали за висячим указателем.

+0

Извините, мне все еще не ясно. Да, поток 1 будет следовать отсутствующему указателю и читать значение в «следующем», которое, вероятно, уже выделено для другой переменной. Итак, второй аргумент для compare_exchange_weak() будет недействительным. Но он никогда не будет использоваться, потому что, когда compare_exchange_weak() будет выполнен, он обнаружит это «head_! = Old_head». Итак, да, мы будем читать недопустимый аргумент для compare_exchange_weak(), но он никогда не будет использоваться. В чем проблема? Не могли бы вы прояснить? – Alex

+0

@alex Почему вы предполагаете, что они не равны? Вы следовали за висячим указателем, значение может быть * ничего *. И где он проверяет 'head _! = Old_head_'? Я пропустил это? – Yakk

+0

Скорее всего, я что-то упускаю, но я до сих пор не могу понять, что именно. Проверьте, что «head_! = Old_head_» находится внутри compare_exchange_weak(). Если эти значения не равны, второй аргумент compare_exchange_weak() не будет использоваться. В моем понимании compare_exchange_weak() атомарно считывает текущее значение головки из памяти. В обсуждаемом сценарии после пробуждения 1-го потока значение головки в регистре не будет равно текущему значению головы в памяти. Таким образом, compare_exchange_weak() увидит это и не будет использовать второй аргумент (мусор, который мы читаем после недопустимого значения head в регистре). – Alex

Смежные вопросы