Параллельное программирование на С++ в действии. Практика разработки многопоточных программ | страница 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 lock_a(lhs.m, std::adopt_lock);←(2)

>  std::lock_guard lock_b(rhs.m, std::adopt_lock);←(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 помогает избежать взаимоблокировки в случаях, когда нужно захватить сразу два или более мьютексов, она не в силах помочь, если мьютексы захватываются порознь, — в таком случае остается полагаться только на дисциплину программирования. Это нелегко, взаимоблокировки — одна из самых неприятных проблем в многопоточной программе, часто они возникают непредсказуемо, хотя в большинстве случаев все работает нормально. Однако все же есть несколько относительно простых правил, помогающих писать свободный от взаимоблокировок код.