Вернемся к коду примера. Хотя код запускался только в контексте одного потока, мы увидели, что он собирался использовать вспомогательные классы для блокировки. Псевдоним типа shrd_lck
shared_lock
и позволяет блокировать экземпляр мьютекса несколько раз в коллективном режиме. До тех пор, пока существуют sl1
и sl2
, никакие вызовы print_exclusive
не могут заблокировать мьютекс в эксклюзивном режиме. Это все еще просто.Теперь перейдем к эксклюзивным функциям блокировки, которые появились позднее в функции main
int main()
{
{
shrd_lck sl1 {shared_mut};
{
shrd_lck sl2 {shared_mut};
print_exclusive();
}
print_exclusive();
}
try {
exclusive_throw();
} catch (int e) {
cout << "Got exception " << e << '\n';
}
print_exclusive();
}
Важная деталь — после возвращения из exclusive_throw
print_exclusive
снова может заблокировать мьютекс, несмотря на то что exclusive_throw
завершила работу некорректно из-за генерации исключения.Еще раз взглянем на функцию print_exclusive
void print_exclusive()
{
uniq_lck l {shared_mut, defer_lock};
if (l.try_lock()) {
// ...
}
}
Мы предоставили shared_mut
defer_lock
в качестве аргументов конструктора для unique_lock
в данной процедуре. defer_lock
— пустой глобальный объект, который послужит для выбора другого конструктора класса unique_lock
, просто не блокирующего мьютекс. Позднее можно вызвать функцию l.try_lock()
, которая не блокирует мьютекс. Если мьютекс уже был заблокирован, то можно сделать что-то еще. При полученной блокировке деструктор поможет выполнить уборку. Избегаем взаимных блокировок с применением std::scoped_lock
Если бы взаимные блокировки происходили на дорогах, то выглядели бы так (рис. 9.2).
Чтобы снова запустить движение, понадобился бы большой кран, который в случайном порядке выбирает по одной машине из центра перекрестка и удаляет ее. Если это невозможно, то достаточное количество водителей должны взаимодействовать друг с другом. Взаимную блокировку могут разрешить все водители, едущие в одном направлении, сдвинувшись на несколько метров назад, позволяя продвинуться остальным водителям.
В многопоточных программах такие ситуации должен разрешать программист. Однако слишком легко потерпеть неудачу, если программа сама по себе довольно сложная.
В этом примере мы напишем код, намеренно создающий взаимную блокировку. Затем увидим, как писать код, который получает те же ресурсы, что привели другой код к взаимной блокировке, но воспользуемся новым классом блокировки STL std::scoped_lock
Как это делается
Код этого примера состоит из двух пар функций, которые должны быть выполнены конкурирующими потоками, они получают два ресурса в форме мьютексов. Одна пара создает взаимную блокировку, а вторая избегает такой ситуации. В функции main мы опробуем их в деле.
1. Сначала включим все необходимые заголовочные файлы и объявим об использовании пространств имен std
chrono_literals
:#include
#include
#include
using namespace std;
using namespace chrono_literals;
2. Затем создадим два объекта мьютексов, которые понадобятся для создания взаимной блокировки:
mutex mut_a;
mutex mut_b;
3. Чтобы создать взаимную блокировку с двумя ресурсами, нужны две функции. Одна пробует заблокировать мьютекс А
В
, а другая сделает это в противоположном порядке. Позволив обеим функциям приостановиться между блокировками, можно гарантировать, что код навсегда попадет во взаимную блокировку. (Это делается только для демонстрации. Программа, не содержащая команд по приостановке потоков, может запуститься успешно и не столкнуться с взаимной блокировкой, если запускать ее раз за разом.) Обратите внимание: мы не используем символ '\n'
для вывода на экран перевода строки, а применяем для данных целей endl
. Он не только выполняет перевод строки, но еще и опустошает буфер потока cout
, поэтому можно убедиться, что операции вывода не объединяются и не откладываются:static void deadlock_func_1()
{
cout << "bad f1 acquiring mutex A..." << endl;
lock_guard
this_thread::sleep_for(100ms);
cout << "bad f1 acquiring mutex B..." << endl;
lock_guard