Стек без блокировки: проблема видимости при проверке указателей опасности во время 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() и прерывается только перед выполнением:

    while(old_head &&
      !head.compare_exchange_strong(old_head,old_head->next));
    
    Таким образом, поток A видит текущую головку как 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 a load() на указателе опасности потока A возвращает последнее значение, сохраненное потоком A.

В основном: мне интересно, Может ли поток B load() устаревшее значение вместо последнего. Сказал с другой стороны, я не уверен, почему он должен возвращать значение, установленное потоком A (old_node).

Где же изъян в этом рассуждении? Я не могу найти оправдания тому, почему hp.store(old_head) на другом потоке произойдет-раньше hazard_pointers[i].pointer.load().
2 2

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(), Вот почему это поток A hp.store(old_head) происходит-перед потоком B compare_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 в текущем потоке можно безопасно удалить, так как он фактически не используется другим потоком.