Параллельное программирование на С++ в действии. Практика разработки многопоточных программ | страница 53
К счастью, в стандартной библиотеке есть на этот случай лекарство в виде функции >std::lock
, которая умеет захватывать сразу два и более мьютексов без риска получить взаимоблокировку. В листинге 3.6 показано, как воспользоваться ей для реализации простой операции обмена.
Листинг 3.6. Применение >std::lock
и >std::lock_guard
для реализации операции обмена
>class some_big_object;
>void swap(some_big_object& lhs, some_big_object& rhs);
>class X {
>private:
> some_big_object some_detail;
> std::mutex m;
>public:
> X(some_big_object const& sd) : some_detail(sd) {}
> friend void swap(X& lhs, X& rhs) {
> if (&lhs == &rhs)
> return;
> std::lock(lhs.m, rhs.m); ←
(1)
> std::lock_guard
(2)
> std::lock_guard
(3)
> swap(lhs.some_detail,rhs.some_detail);
> }
>};
Сначала проверяется, что в аргументах переданы разные экземпляры, постольку попытка захватить >std::mutex
, когда он уже захвачен, приводит к неопределенному поведению. (Класс мьютекса, допускающего несколько захватов в одном потоке, называется >std::recursive_mutex
. Подробности см. в разделе 3.3.3.) Затем мы вызываем >std::lock()
(1), чтобы захватить оба мьютекса, и конструируем два экземпляра >std::lock_guard
(2), (3) — по одному для каждого мьютекса. Помимо самого мьютекса, конструктору передается параметр >std::adopt_lock
, сообщающий объектам >std::lock_guard
, что мьютексы уже захвачены, и им нужно лишь принять владение существующей блокировкой, а не пытаться еще раз захватить мьютекс в конструкторе.
Это гарантирует корректное освобождение мьютексов при выходе из функции даже в случае, когда защищаемая операция возбуждает исключение, а также возврат результата сравнения в случае нормального завершения. Стоит также отметить, что попытка захвата любого мьютекса >lhs.m
или >rhs.m
внутри >std::lock
может привести к исключению; в этом случае исключение распространяется на уровень функции, вызвавшей >std::lock
. Если >std::lock
успешно захватила первый мьютекс, но при попытке захватить второй возникло исключение, то первый мьютекс автоматически освобождается; >std::lock
обеспечивает семантику «все или ничего» в части захвата переданных мьютексов.
Хотя >std::lock
помогает избежать взаимоблокировки в случаях, когда нужно захватить сразу два или более мьютексов, она не в силах помочь, если мьютексы захватываются порознь, — в таком случае остается полагаться только на дисциплину программирования. Это нелегко, взаимоблокировки — одна из самых неприятных проблем в многопоточной программе, часто они возникают непредсказуемо, хотя в большинстве случаев все работает нормально. Однако все же есть несколько относительно простых правил, помогающих писать свободный от взаимоблокировок код.