Стек без блокировки: проблема видимости при проверке указателей опасности во время 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_head
iff 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
в текущем потоке можно безопасно удалить, так как он фактически не используется другим потоком.