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

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

3.2.8. Выбор правильной гранулярности блокировки

О гранулярности блокировок я уже упоминал в разделе 3.2.3: под этим понимается объем данных, защищаемых блокировкой. Мелкогранулярные блокировки защищают мало данных, крупногранулярные — много. Важно не только выбрать подходящую гранулярность, но и позаботиться о том, чтобы блокировка удерживалась не дольше, чем реально необходимо. Все мы сталкивались с ситуацией, когда очередь к кассе в супермаркете перестает двигаться из-за того, что обслуживаемый покупатель вдруг выясняет, что забыл прихватить баночку соуса, и отправляется за ней, заставляя всех ждать, или из-за того, что кассирша уже готова принять деньги, а покупатель только— только полез за кошельком. Насколько было бы проще, если бы каждый подходил к кассе только после того, как купил все необходимое и подготовился оплатить покупки.

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

Объект std::unique_lock отлично приспособлен для таких ситуаций, потому что можно вызвать его метод unlock(), когда программе не нужен доступ к разделяемым данным, а затем вызвать lock(), если доступ снова понадобится:

void get_and_process_data() (1) Во время работы process() зах-

{                          ←┘ ватывать мьютекс не нужно

 std::unique_lock<std::mutex> my_lock(the_mutex);

 some_class data_to_process = get_next_data_chunk();

 my_lock.unlock();

 result_type result = process(data_to_process);

 my_lock.lock();                      ←┐ Снова захватить мью-

 write_result(data_to_process, result);│ текс перед записью

}                                      (2) результатов

Удерживать мьютекс на время выполнения process() нет необходимости, поэтому мы вручную освобождаем его перед вызовом (1) и снова захватываем после возврата (2).

Очевидно, что если один мьютекс защищает структуру данных целиком, то не только возрастает конкуренция за него, но и шансов снизить время удержания остается меньше. Поскольку под защитой одного мьютекса приходится выполнять больше операций, то и удерживать его нужно дольше. Такая двойная угроза должна вдвое усилить стремление всюду, где возможно, использовать мелкогранулярные блокировки.