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