Собираясь удалить объект, поток должен сначала проверить указатели опасности в других имеющихся в системе потоках. Если ни один из указателей опасности не ссылается на данный объект, то его можно спокойно удалять. В противном случае удаление следует отложить. Периодически поток просматривает список отложенных объектов в поисках тех, которые уже можно удалить.

Высокоуровневое описание выглядит достаточно простым, но как это сделать на С++?

Прежде всего, необходимо место для хранения указателя на интересующий нас объект — сам указатель опасности. Это место должно быть видно из всех потоков, причем указатель опасности должен существовать в каждом потоке, который может получить доступ к структуре данных. Корректное и эффективное выделение такого места — непростая задача, поэтому отложим ее на потом, а пока предположим, что существует функция get_hazard_pointer_for_current_thread(), которая возвращает ссылку на указатель опасности. Затем нужно установить указатель опасности перед чтением указателя, который мы намерены разыменовать, — в данном случае указателя head на начало списка:

std::shared_ptr pop() {

 std::atomic& hp =

  get_hazard_pointer_for_current_thread();

 node* old_head = head.load();←(1)

 node* temp;

 do {

  temp = old_head;

  hp.store(old_head);         ←(2)

  old_head = head.load();

 } while (old_head != temp);  ←(3)

 // ...

}

Это необходимо делать в цикле while, чтобы узел node случайно не был удалён между чтением старого указателя head (1) и установкой указателя опасности (2). В течение этого промежутка времени ни один поток не знает, что мы собираемся обратиться к этому узлу. К счастью, если кто-то собирается удалить старый узел head, то сам указатель head должен был быть изменен, так что мы можем это проверить и не выходить из цикла, пока не будем твердо уверены, что указатель head по-прежнему имеет то же значение, которое было записано в указатель опасности (3). Такое использование указателей опасности опирается на тот факт, что можно безопасно использовать значение указателя даже после того, как объект, на который он указывает, уже удалён. Технически для стандартных реализаций new и delete это считается неопределенным поведением, поэтому либо убедитесь, что ваша реализация стандартной библиотеки допускает такое использование, либо реализуйте собственный распределитель.

Установив указатель опасности, мы можем продолжить выполнение pop(), будучи уверены, что ни один другой поток не попытается «вытащить» из-под нас узлы. Ну почти уверены: при каждом перечитывании old_head необходимо обновлять указатель опасности перед тем, как разыменовывать вновь прочитанное значение указателя. После того как узел извлечён из списка, мы можем очистить наш собственный указатель опасности. Если на наш узел не ссылаются другие указатели опасности, то его можно удалять; в противном случае его следует поместить в список узлов, ожидающих удаления. В листинге ниже приведен полный код функции pop(), реализованной по такой схеме.

Листинг 7.6. Реализация функции pop() с помощью указателей опасности

std::shared_ptr pop() {

 std::atomic& hp =

  get_hazard_pointer_for_current_thread();

 node* old_head = head.load();

 do {

  node* temp;(1) Цикл, пока указатель

  do         ←┤опасности не установлен

  {           │на head

   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);←(2) Закончив, очищаем указатель опасности

 std::shared_ptr res;

 if (old_head) {

  res.swap(old_head->data);

  if (outstanding_hazard_pointers_for(old_head))←┐Прежде чем

  {                                              ├(3) удалять узел,

   reclaim_later(old_head);                      │проверяем,(4)

  }                                              │нет ли ссы-

  else                                           │лающихся на

Перейти на страницу:

Похожие книги