Стек без блокировки: проблема видимости при проверке указателей опасности во время pop ()?
Я читаю Энтони Уильяма параллелизм C++ в действии. Глава 7 описывает процесс разработки стека без блокировки и иллюстрирует общие проблемы, которые затрудняют Программирование без блокировки. В частности, в разделе 7.2.3 (обнаружение узлов, которые не могут быть восстановлены с помощью указателей опасности) описывается, как указатели опасности можно использовать, чтобы избежать гонки данных и убедиться, что другие потоки не delete узел все еще ссылается на другой поток.
Этот код является одним из итерации pop(), иллюстрированные в этой главе:
std::shared_ptr<T> pop()
{
std::atomic<void*>& hp = get_hazard_pointer_for_current_thread();
node* old_head = head.load();
do
{
node* temp;
do
{
temp = old_head;
hp.store(old_head);
old_head = head.load();
} while(old_head != temp);
}
while(old_head &&
!head.compare_exchange_strong(old_head,old_head->next));
hp.store(nullptr);
std::shared_ptr<T> res;
if(old_head)
{
res.swap(old_head->data);
if(outstanding_hazard_pointers_for(old_head))
{
reclaim_later(old_head);
}
else
{
delete old_head;
}
delete_nodes_with_no_hazards();
}
return res;
}
У меня есть сомнения по поводу этого фрагмента:
if(outstanding_hazard_pointers_for(old_head))
{
reclaim_later(old_head);
}
else
{
delete old_head;
}
Цель указателей опасности состоит в том, чтобы убедиться, что old_head удаляется, когда никакие другие потоки не могут все еще использовать его. Предлагаемая реализация outstanding_hazard_pointers_for выглядит следующим образом:
unsigned const max_hazard_pointers=100;
struct hazard_pointer
{
std::atomic<std::thread::id> id;
std::atomic<void*> pointer;
};
hazard_pointer hazard_pointers[max_hazard_pointers];
bool outstanding_hazard_pointers_for(void* p)
{
for(unsigned i=0; i < max_hazard_pointers; ++i)
{
if(hazard_pointers[i].pointer.load() == p)
{
return true;
}
}
return false;
}
В основном массив указателей опасности сканируется, чтобы проверить, присутствует ли указатель на искомый узел. Мне интересно, почему эта операция действительно безопасна. Атомарный load() выполняется и даже если используется последовательное последовательное упорядочение, load() может загрузить устаревшее значение. Как следствие, p может не быть найден, и pop() будет удалять узел, который все еще используется.
Представьте, что происходит следующее:
-
Поток A начинает выполнять
pop()и прерывается только перед выполнением:
Таким образом, поток A видит текущую головку какwhile(old_head && !head.compare_exchange_strong(old_head,old_head->next));old_head, которая сохраняется в его указателе опасности.old_headбудет разыменован, когда нить просыпается и пытается высунуть голову, призываяhead.compare_exchange_strong(old_head, old_head->next). -
Поток B начинает вызывать
pop()вплоть доif(outstanding_hazard_pointers_for(old_head))old_headбудет текущей головкой стека, то есть тем же самым узлом, на который поток A ссылается какold_head. Поток B не будетdelete old_headiff aload()на указателе опасности потока A возвращает последнее значение, сохраненное потоком A.
В основном: мне интересно, Может ли поток B load() устаревшее значение вместо последнего. Сказал с другой стороны, я не уверен, почему он должен возвращать значение, установленное потоком A (old_node).
hp.store(old_head) на другом потоке произойдет-раньше hazard_pointers[i].pointer.load().2 ответа:
Я отвечаю на свой собственный вопрос по двум причинам: я думаю, что ответ, который я принял, не очень ясен, и комментарий JJ15k подтверждает это впечатление.
В основном ключ заключается в том, чтобы наблюдать, что для другого потока, чтобы сделать это через
if(outstanding_hazard_pointers_for(old_head))и видя то же самоеold_head, увиденное другим потоком, который был опережен перед выполнениемwhile(old_head && !head.compare_exchange_strong(old_head, old_head->next)), он должен был выполнитьhead.compare_exchange_strong(old_head, old_head->next)с тем жеold_head. Но тогда (предполагая, что<указывает на то, что происходит-до отношения):thread A: hp.store(old_head) < thread A: old_head = head.load() < thread B: head.compare_exchange_strong(old_head, old_head->next)Помните, что поток B видит то же самое
old_headмы загрузили первую инструкцию, и она меняет ее значение наold_head->next. Мы все еще видим то же самое значение вhead.load(), Вот почему это поток Ahp.store(old_head)происходит-перед потоком Bcompare_exchange_strong.Таким образом, поток, который собирается проверить, можно ли удалить головку, содержащуюся в указателе опасности , имеет, чтобы увидеть
Я надеюсь, что это ответ на ваш вопрос.old_head. Также обратите внимание на фундаментальную роль, которую играетold_head = head.load()(и цикл, который содержит те утверждения, которые на первый взгляд могут показаться излишними). Без этой операцииloadне было бы никакого отношения междуstoreизold_headвhpиcompare_exchange_strong.
Мое понимание кода заключается в следующем.
Если
hp.store(old_head)в другом потоке не произошло-до вызоваhazard_pointers[i].pointer.load()в этом потоке, это означает, что этот поток успешно выполнил вызовhead.compare_exchange_strong(old_head,old_head->next). Это означает, что для другого потокаold_head != temp, поэтому он выполнит еще одну попытку сохранить правильныйold_headкак потокhp.И это означает, что исходный указатель
old_headв текущем потоке можно безопасно удалить, так как он фактически не используется другим потоком.