Выбрать главу

К счастью, в стандартной библиотеке есть на этот случай лекарство в виде функции 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<std::mutex> lock_a(lhs.m, std::adopt_lock);← (2)

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

3.2.5. Дополнительные рекомендации, как избежать взаимоблокировок

Взаимоблокировка может возникать не только при захвате мьютексов, хотя это и наиболее распространенная причина. Нарваться на взаимоблокировку можно и тогда, когда есть два потока и никаких мьютексов; достаточно, чтобы каждый поток вызвал функцию jоin() объекта std::thread, связанного с другим потоком. В этом случае ни один поток не сможет продолжить выполнение, потому что будет ждать завершения другого потока, — точно так же, как дети, ссорящиеся по поводу игрушек. Такой простой цикл может возникнуть всюду, где один поток ждет завершения другого для продолжения работы, а этот другой поток одновременно ждет завершения первого. Причем потоков необязательно должно быть два — цикл, в котором участвуют три и более потоков, также приведёт к взаимоблокировке. Общая рекомендация во всех случаях одна: не ждите завершения другого потока, если есть малейшая возможность, что он будет дожидаться вас. Ниже приведены конкретные советы, как выявить и устранить такую возможность.