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

 friend void swap(X& lhs, X& rhs) {

  if (&lhs == &rhs)                     std::defer_lock оставляет

   return;                              мьютексы не захваченными (1)

   std::unique_lock<std::mutex> lock_a(lhs.m, std::defer_lock);←┤

   std::unique_lock<std::mutex> lock_b(rhs.m, std::defer_lock);←┘

   std::lock(lock_a, lock_b); ← (2) Мьютексы захватываются

   swap(lhs.some_detail, rhs.some_detail);

 }

};

В листинге 3.9 объекты std::unique_lock можно передавать функции std::lock() (2), потому что в классе std::unique_lock имеются функции-члены lock(), try_lock() и unlock(). Для выполнения реальной работы они вызывают одноименные функции контролируемого мьютекса, а сами только поднимают в экземпляре std::unique_lock флаг, показывающий, что в данный момент этот экземпляр владеет мьютексом. Флаг необходим для того, чтобы деструктор знал, вызывать ли функцию unlock(). Если экземпляр действительно владеет мьютексом, то деструктор должен вызвать unlock(), в противном случае — не должен. Опросить состояние флага позволяет функция-член owns_lock().

Естественно, этот флаг необходимо где-то хранить. Поэтому размер объекта std::unique_lock обычно больше, чем объекта std::lock_guard, и работает std::unique_lock чуть медленнее std::lock_guard, потому что флаг нужно проверять и обновлять. Если класс std::lock_guard отвечает вашим нуждам, то я рекомендую использовать его. Тем не менее, существуют ситуации, когда std::unique_lock лучше отвечает поставленной задаче, так как без свойственной ему дополнительной гибкости не обойтись. Один из примеров — показанный выше отложенный захват; другой — необходимость передавать владение мьютексом из одного контекста в другой.

3.2.7. Передача владения мьютексом между контекстами

Поскольку экземпляры std::unique_lock не владеют ассоциированными мьютексами, то можно передавать владение от одного объекта другому путем перемещения. В некоторых случаях передача производится автоматически, например при возврате объекта из функции, а иногда это приходится делать явно, вызывая std::move(). Ситуация зависит от того, является ли источник l-значением — именованной переменной или ссылкой на нее — или r-значением — временным объектом. Если источник — r-значение, то передача владения происходит автоматически, в случае же l-значение это нужно делать явно, чтобы не получилось так, что переменная потеряет владение непреднамеренно. Класс std::unique_lock дает пример перемещаемого, но не копируемого типа. Дополнительные сведения о семантике перемещения см. в разделе А.1.1 приложения А.

Одно из возможных применений — разрешить функции захватить мьютекс, а потом передать владение им вызывающей функции, чтобы та могла выполнить дополнительные действия под защитой того же мьютекса. Ниже приведен соответствующий пример — функция get_lock() захватывает мьютекс, подготавливает некоторые данные, а потом возвращает мьютекс вызывающей программе:

std::unique_lock<std::mutex> get_lock() {

 extern std::mutex some_mutex;

 std::unique_lock<std::mutex> lk(some_mutex);

 prepare_data();

 return lk; ← (1)

}

void process_data() {

 std::unique_lock<std::mutex> lk(get_lock()); ← (2)

 do_something();

}

Поскольку lk — автоматическая переменная, объявленная внутри функции, то ее можно возвращать непосредственно (1), не вызывая std:move(); компилятор сам позаботится о вызове перемещающего конструктора. Затем функция process_data() может передать владение своему экземпляру std::unique_lock (2), и do_something() может быть уверена, что подготовленные данные не были изменены каким-то другим потоком.

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