C++17 STL Стандартная библиотека шаблонов — страница 99 из 119

аться об этом не нужно.

Единственное, о чем следует волноваться, — это объединение. Когда поток вызывает функцию

x.join()
для объекта другого потока, его выполнение приостанавливается до того, как будет выполнен поток
x
. Обратите внимание: нас ничто не спасет при попадании потока в бесконечный цикл! Если нужно, чтобы поток продолжал существовать до тех пор, пока не решит завершиться, то можно вызвать функцию
x.detach()
. После этого у нас не будет возможности управлять потоком. Независимо от принятого решения, мы должны всегда объединять или откреплять потоки. Если мы не сделаем этого, то деструктор объекта
thread
вызовет функцию
std::terminate()
, что приведет к внезапному завершению работы приложения.

В момент, когда функция main возвращает значение, приложение заканчивает работу. Однако в это же время наш открепленный поток

t3
все еще находится в приостановленном состоянии и не успевает отправить сообщение bye на консоль. Операционной системе это неважно: она просто завершает всю программу, не дожидаясь завершения данного потока. Указанный факт важно иметь в виду. Если дополнительный поток должен был соревноваться за что-то важное, то нужно было бы подождать его завершения в функции
main

Выполняем устойчивую к исключениям общую блокировку с помощью td::unique_lock и std::shared_lock

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

В этом разделе мы взглянем на классы-мьютексы STL и абстракции блокировки RAII. Поэкспериментируем с ними в нашей конкретной реализации примера, а также изучим другие вспомогательные средства синхронизации, предоставляемые STL.


Как это делается

В этом примере мы напишем программу, которая использует экземпляр класса

std::shared_mutex
в эксклюзивном и коллективном режимах, и увидим, что это значит. Кроме того, не будем вызывать функции
lock
и
unlock
самостоятельно, а сделаем это с помощью вспомогательных функций RAII.


1. Сначала включим все необходимые заголовочные файлы. Поскольку мы задействуем функции и структуры данных STL, а также временные литералы, объявим об использовании пространств имен

std
и
chrono_literal
:


#include 

#include 

#include 

#include 


using namespace std;

using namespace chrono_literals;


2. Вся программа строится вокруг одного общего мьютекса, поэтому для простоты объявим его глобальный экземпляр:


shared_mutex shared_mut;


3. Мы будем использовать вспомогательные функции RAII

std::shared_lock
и
std::unique_lock
. Чтобы их имена выглядели более понятными, определим для них короткие псевдонимы:


using shrd_lck = shared_lock;

using uniq_lck = unique_lock;


4. Прежде чем начнем писать функцию main, определим две вспомогательные функции, которые пытаются заблокировать мьютекс в эксклюзивном режиме. Эта функция создаст экземпляр класса

unique_lock
для общего мьютекса. Второй аргумент конструктора
defer_lock
указывает объекту поддерживать блокировку снятой. В противном случае его конструктор попробует заблокировать мьютекс, а затем будет удерживать его до завершения. Далее вызываем метод
try_lock
для объекта
exclusive_lock
. Этот вызов немедленно вернет булево значение, которое говорит, получили мы блокировку или же мьютекс уже был заблокирован кем-то еще.


static void print_exclusive()

{

  uniq_lck l {shared_mut, defer_lock};

  if (l.try_lock()) {

    cout << "Got exclusive lock.\n";

  } else {

    cout << "Unable to lock exclusively.\n";

  }

}


5. Другая вспомогательная функция также пытается заблокировать мьютекс в эксклюзивном режиме. Она делает это до тех пор, пока не получит блокировку. Затем мы симулируем какую-нибудь ошибку, генерируя исключение (содержащее лишь простое целое число). Несмотря на то, что это приводит к мгновенному выходу контекста, в котором мы хранили заблокированный мьютекс, последний будет освобожден. Это происходит потому, что деструктор объекта

unique_lock
освободит блокировку в любом случае по умолчанию.


static void exclusive_throw()

{

  uniq_lck l {shared_mut};

  throw 123;

}


6. Теперь перейдем к функции

main
. Сначала откроем еще одну область видимости и создадим экземпляр класса
shared_lock
. Его конструктор мгновенно заблокирует мьютекс в коллективном режиме. Мы увидим, что это значит, в следующих шагах.


int main()

{

  {

    shrd_lck sl1 {shared_mut};

    cout << "shared lock once.\n";


7. Откроем еще одну область видимости и создадим второй экземпляр типа

shared_lock
для того же мьютекса. Теперь у нас есть два экземпляра типа
shared_lock
, и оба содержат общую блокировку мьютекса. Фактически можно создать произвольно большое количество экземпляров типа
shared_lock
для одного мьютекса. Затем вызываем функцию
print_exclusive
, которая пытается заблокировать мьютекс в эксклюзивном режиме. Эта операция не увенчается успехом, поскольку он уже находится в коллективном режиме.


    {

      shrd_lck sl2 {shared_mut};

      cout << "shared lock twice.\n";

      print_exclusive();

    }


8. После выхода из самой поздней области видимости деструктор объекта

sl2
типа
shared_lock
освобождает свою общую блокировку мьютекса. Функция
print_exclusive
снова даст сбой, поскольку мьютекс все еще находится в коллективном режиме блокировки.


    cout << "shared lock once again.\n";

    print_exclusive();

  }

  cout << "lock is free.\n";


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