9. После выхода из второй области видимости все объекты типа shared_lock
exclusive_throw
и print_exclusive
. Помните, что мы генерируем исключение в вызове exclusive_throw
. Но поскольку unique_lock
— это объект RAII, который помогает защититься от исключений, мьютекс снова будет разблокирован независимо от того, что вернет вызов exclusive_throw
. Таким образом, функция print_exclusive
не будет ошибочно блокировать все еще заблокированный мьютекс: try {
exclusive_throw();
} catch (int e) {
cout << "Got exception " << e << '\n';
}
print_exclusive();
}
10. Компиляция и запуск программы дадут следующий результат. Первые две строки показывают наличие двух экземпляров общей блокировки. Затем функция print_exclusive
print_exclusive
все еще будет давать сбой. После выхода из второй области видимости, что наконец снова освободит мьютекс, функции exclusive_throw
и print_exclusive
смогут заблокировать мьютекс:$ ./shared_lock
shared lock once.
shared lock twice.
Unable to lock exclusively.
shared lock once again.
Unable to lock exclusively.
lock is free.
Got exception 123
Got exclusive lock.
Как это работает
При просмотре документации С++ может показаться несколько странным факт существования разных классов мьютексов и абстракций блокировок RAII. Прежде чем рассматривать наш конкретный пример кода, подытожим все, что может предложить STL.
Классы мьютексов
Термин mutex расшифровывается как mu
tual exclusion (взаимное исключение). Чтобы предотвратить неуправляемое изменение одного объекта несколькими конкурирующими потоками, способное привести к повреждению данных, можно использовать объекты мьютексов. STL предоставляет разные классы мьютексов, которые хороши в разных ситуациях. Все они похожи в том, что имеют методыlock
и unlock
.Когда некий поток первым вызывает метод lock()
lock
до тех пор, пока первый поток не вызовет снова метод unlock
. Класс std::mutex
может делать именно это.В STL существует множество разных классов мьютексов (табл. 9.2).
Классы блокировок
Работать с многопоточностью легко и просто до тех пор, пока потоки просто блокируют мьютекс, получают доступ к защищенному от конкурентности объекту, а затем снова разблокируют мьютекс. Как только программист забывает разблокировать мьютекс после одной из блокировок, ситуация значительно усложняется.
В лучшем случае программа просто мгновенно зависает, и вызов, не выполняющий разблокировку, быстро выявляется. Такие ошибки, однако, очень похожи на утечки памяти, которые случаются, если отсутствуют вызовы delete
Для управления памятью существуют вспомогательные классы unique_ptr
shared_ptr
и weak_ptr
. Они предоставляют очень удобный способ избежать утечек памяти. Такие классы существуют и для мьютексов. Простейшим из них является std::lock_guard
. Его можно использовать следующим образом:void critical_function()
{
lock_guard
// критический раздел
}
Конструктор элемента lock_guard
lock
. Весь вызов конструктора заблокируется до тех пор, пока тот не получит блокировку для мьютекса. При разрушении объекта он разблокирует мьютекс. Таким образом, понять цикл блокировки/разблокировки сложно, поскольку она происходит автоматически.В STL версии C++17 предоставляются следующие разные абстракции блокировок RAII (табл. 9.3). Все они принимают аргумент шаблона, который будет иметь тот же тип, что и мьютекс (однако, начиная с C++17, компилятор может вывести этот тип самостоятельно).
В то время как lock_guard
scoped_lock
имеют простейшие интерфейсы, которые состоят только из конструктора и деструктора, unique_lock
и shared_lock
более сложны, но и более гибки. В последующих примерах мы увидим, как еще их можно использовать, помимо простой блокировки.