Если счетчик после уменьшения pop() в промежутке между первой проверкой threads_in_pop (1) и «заявлением прав» на список (2) и добавить в список узлы, к которым все еще обращается один или несколько потоков. На рис. 7.1 поток С добавляет узел Y в список to_be_deleted, несмотря на то, что поток В все еще ссылается на него по указателю old_head и, значит, будет пробовать читать его указатель next. Поэтому поток А не может удалить узлы без риска вызвать неопределенное поведение в потоке В.
Рис. 7.1. На примере трех потоков, вызывающих pop(), видно, почему необходимо проверять threads_in_pop после заявления прав на список узлов, ожидающих удаления, в try_reclaim()
Чтобы поместить узлы, ожидающие удаления, в список ожидающих, мы используем уже имеющийся указатель next для связывания. Для того чтобы добавить цепочку в список, мы проходим до конца цепочки (9), записываем в указатель next в последнем узле текущее значение to_be_deleted (10) и сохраняем указатель на первый узел цепочки как новый указатель to_be_deleted (11). Здесь необходимо вызывать compare_exchange_weak в цикле, чтобы не допустить утечки узлов, добавленных другим потоком. В результате в next записывается указатель на последний узел цепочки, если он изменился. Добавление единственного узла в список — это особый случай, когда первый узел в добавляемой цепочке совпадает с последним (12).
Этот алгоритм работает вполне приемлемо, если нагрузка невелика, то есть существуют моменты pop() нет ни одного потока. Но эта ситуация кратковременна; именно поэтому мы должны проверять, что счетчик threads_in_pop после уменьшения обратился в нуль (3), прежде чем освобождать память, и по той же причине проверка стоит threads_in_pop равно 1, и попыткой удалить узлы, тем больше шансов, что какой-то другой поток вызовет pop(), после чего threads_in_pop перестанет быть равно 1 и, стало быть, удалять узлы станет нельзя.
Если нагрузка высока, то затишье может не наступить pop() до того, как пребывавшие там успевают выйти. В таком случае список to_be_deleted будет расти неограниченно, и мы снова сталкиваемся с утечкой памяти. Если периодов затишья не ожидается, то необходим другой механизм освобождения узлов. Главное здесь — определить, когда ни один поток больше не обращается к конкретному узлу, который, следовательно, можно освободить. Из всех возможных механизмов такого рода для понимания проще всего тот, в котором используются
7.2.3. Обнаружение узлов, не подлежащих освобождению, с помощью указателей опасности
Термин