аться об этом не нужно.
Единственное, о чем следует волноваться, — это объединение. Когда поток вызывает функцию
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