std::thread t(my_func);
try {
do_something_in_current_thread()
}
catch(...) {
t.join(); ←
(1)
throw;
}
t.join(); ←
(2)
}
В листинге 2.2 блок try
/catch
используется для того, чтобы поток, имеющий доступ к локальному состоянию, гарантированно завершился до выхода из функции вне зависимости оттого, происходит выход нормально (2) или вследствие исключения (1). Записывать блоки try
/catch
очень долго и при этом легко допустить ошибку, поэтому такой способ не идеален. Если необходимо гарантировать, что поток завершается до выхода из функции потому ли, что он хранит ссылки на локальные переменные, или по какой-то иной причине то важно обеспечить это на всех возможных путях выхода, как нормальных, так и в результате исключения, и хотелось бы иметь для этого простой и лаконичный механизм.
Один из способов решить эту задачу воспользоваться стандартной идиомой захват ресурса есть инициализация (RAII) и написать класс, который вызывает join()
в деструкторе, например, такой, как в листинге 2.3. Обратите внимание, насколько проще стала функция f()
.
Листинг 2.3. Использование идиомы RAII для ожидания завершения потока
class thread_guard {
std::threads t;
public:
explicit thread_guard(std::thread& t_) : t(t_) {}
~thread_guard() {
if (t.joinable()) ←
(1)
{
t.join(); ←
(2)
}
}
thread_guard(thread_guard const&)=delete; ←
(3)
thread_guard& operator=(thread_guard const&)=delete;
};
struct func; ←┐
см.определение
│
в листинге 2.1
void f() {
int some_local_state;
std::thread t(func(some_local_state));
thread_guard g(t);
do_something_in_current_thread();
} ←
(4)
Когда текущий поток доходит до конца f
(4), локальные объекты уничтожаются в порядке, обратном тому, в котором были сконструированы. Следовательно, сначала уничтожается объект g
типа thread_guard
, и в его деструкторе (2) происходит присоединение к потоку Это справедливо даже в том случае, когда выход из функции f
произошел в результате исключения внутри функции do_something_in_current_thread
.
Деструктор класса thread_guard
в листинге 2.3 сначала проверяет, что объект std::thread
находится в состоянии joinable()
(1) и, лишь если это так, вызывает join()
(2). Это существенно, потому что функцию join()
можно вызывать только один раз для данного потока, так что если он уже присоединился, то делать это вторично было бы ошибкой.
Копирующий конструктор и копирующий оператор присваивания помечены признаком =delete
(3), чтобы компилятор не генерировал их автоматически: копирование или присваивание такого объекта таит в себе опасность, поскольку время жизни копии может оказаться дольше, чем время жизни присоединяемого потока. Но раз эти функции объявлены как «удаленные», то любая попытка скопировать объект типа thread_guard
приведет к ошибке компиляции. Дополнительные сведения об удаленных функциях см. в приложении А, раздел А.2.
Если ждать завершения потока не требуется, то от проблемы безопасности относительно исключений можно вообще уйти, отсоединив поток. Тем самым связь потока с объектом std::thread
разрывается, и при уничтожении объекта std::thread
функция std::terminate()
не будет вызвана. Но отсоединенный поток по-прежнему работает — в фоновом режиме.
2.1.4. Запуск потоков в фоновом режиме
Вызов функции-члeнa detach()
объекта std::thread
оставляет поток работать в фоновом режиме, без прямых способов коммуникации с ним. Теперь ждать завершения потока не получится — после того как поток отсоединен, уже невозможно получить ссылающийся на него объект std::thread
, для которого можно было бы вызвать join()
. Отсоединенные потоки действительно работают в фоне: отныне ими владеет и управляет библиотека времени выполнения С++, которая обеспечит корректное освобождение связанных с потоком ресурсов при его завершении.