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()
в примере выше), который захватит мьютекс. Затем для доступа к данным вызываются функции-члены объекта-привратника. По завершении операции привратник уничтожается, при этом мьютекс освобождается, открывая другим потокам доступ к защищенным данным. Такой объект-привратник вполне может быть перемещаемым (чтобы его можно было возвращать из функции), и тогда тот его член, в котором хранится блокировка, также должен быть перемещаемым.