lk.unlock();
if (p(*next->data)) {←
(15)
return next->data; ←
(16)
}
current = next;
lk = std::move(next_lk);
}
return std::shared_ptr();
}
template ←
(17)
void remove_if(Predicate p) {
node* current = &head;
std::unique_lock lk(head.m);
while(node* const next = current->next.get()) {
std::unique_lock next_lk(next->m);
if (p(*next->data)) { ←
(18)
std::unique_ptr old_next = std::move(current->next);
current->next = std::move(next->next);←
(19)
next_lk.unlock();
} ←
(20)
else {
lk.unlock();←
(21)
current = next;
lk = std::move(next_lk);
}
}
}
};
Показанный в листинге 6.13 шаблон threadsafe_list<>
— это реализация односвязного списка, в котором каждый элемент является структурой типа node
(1). В роли головы head
списка выступает сконструированный по умолчанию объект node
, в котором указатель next
равен NULL
(2). Новые узлы добавляются в список функцией push_front()
; сначала новый узел конструируется (4), при этом для хранимых в нем данных выделяется память из кучи (3), но указатель next
остается равным NULL
. Затем мы должны захватить мьютекс для узла head
, чтобы получить нужное значение указателя next (5), после чего вставить узел в начало списка, записав в head.next
указатель на новый узел (6). Пока всё хорошо: для добавления элемента в список необходимо захватить только один мьютекс, поэтому никакого риска взаимоблокировки нет. Кроме того, медленная операция выделения памяти происходит вне блокировки, так что блокировка защищает только обновление двух указателей — действия, которые не могут привести к ошибке. Переходим к функциям итерирования.
Для начала рассмотрим функцию for_each()
(7). Она принимает объект Function
, который применяется к каждому элементу списка; следуя примеру большинства библиотечных алгоритмов, этот объект передаётся по значению и может быть как настоящей функцией, так и объектом класса, в котором определена оператор вызова. В данном случае функция должна принимать в качестве единственного параметра значение типа T
. Здесь мы производим передачу блокировки. Сначала захватывается мьютекс в головном узле head
(8). Теперь можно безопасно получить указатель на следующий узел next
(с помощью get()
, потому что мы не принимаем на себя владение указателем). Если этот указатель не равен NULL
(9), то захватываем мьютекс в соответствующем узле (10), чтобы обработать данные. Получив блокировку для этого узла, мы можем освободить блокировку для предыдущего узла (11)и вызвать указанную функцию (12). По выходе из этой функции мы можем обновить указатель current
на только что обработанный узел и с помощью move
передать владение блокировкой от next_lk
в lk
(13). Поскольку for_each
передаёт каждый элемент данных напрямую пользовательской функции Function
, мы можем обновить данные, скопировать их в другой контейнер и вообще сделать всё, что угодно. Если функция не делает того, чего нельзя, то это безопасно, потому что на всем протяжении вызова удерживается мьютекс узла, содержащего элемент данных.
Функция find_first_if()
(14)аналогична for_each()
; существенное отличие заключается в том, что переданный предикат Predicate
должен вернуть true
, если нужный элемент найден, и false
в противном случае (15). Если элемент найден, то мы сразу возвращаем хранящиеся в нем данные (16), прерывая поиск. Можно было бы добиться того же результата с помощью for_each()
, но тогда мы продолжили бы просмотр списка до конца, хотя после обнаружения искомого элемента в этом уже нет необходимости.
Функция remove_if()
(17)несколько отличается, потому что она должна изменить список; for_each()
для этой цели непригодна. Если предикат Predicate
возвращает true
(18), то мы удаляем узел из списка, изменяя значение current->next
(19). Покончив с этим, мы можем освободить удерживаемый мьютекс следующего узла. Узел удаляется, когда объект std::unique_ptr
, в который мы его переместили, покидает область видимости (20). В данном случае мы не изменяем current
, потому что необходимо проверить следующий узел next
. Если Predicate
возвращает false
, то нужно просто продолжить обход списка, как и раньше (21).
Читать дальше