Функция join()
дает очень простую и прямолинейную альтернативу — либо мы ждем завершения потока, либо нет. Если необходим более точный контроль над ожиданием потока, например если необходимо проверить, завершился ли поток, или ждать только ограниченное время, то следует прибегнуть к другим механизмам, таким, как условные переменные и будущие результаты, которые мы будем рассматривать в главе 4. Кроме тот, при вызове join()
очищается вся ассоциированная с потоком память, так что объект std::thread
более не связан с завершившимся потоком — он вообще не связан ни с каким потоком. Это значит, что для каждого потока вызвать функцию join()
можно только один раз; после первого вызова объект std::thread
уже не допускает присоединения, и функция joinable()
возвращает false
.
2.1.3. Ожидание в случае исключения
Выше уже отмечалось, что функцию join()
или detach()
необходимо вызвать до уничтожения объекта std::thread
. Если вы хотите отсоединить поток, то обычно достаточно вызвать detach()
сразу после его запуска, так что здесь проблемы не возникает. Но если вы собираетесь дождаться завершения потока, то надо тщательно выбирать место, куда поместить вызов join()
. Важно, чтобы из-за исключения, произошедшего между запуском потока и вызовом join()
, не оказалось, что обращение к join()
вообще окажется пропущенным.
Чтобы приложение не завершилось аварийно при возникновении исключения, необходимо решить, что делать в этом случае. Вообще говоря, если вы намеревались вызвать функцию join()
при нормальном выполнении программы, то следует вызывать ее и в случае исключения, чтобы избежать проблем, связанных с временем жизни. В листинге 2.2 приведен простой способ решения этой задачи.
Листинг 2.2. Ожидание завершения потока
struct func; ←┐
см. определение
│
в листинге 2.1
void f() {
int some_local_state = 0;
func my_func(some_local_state)
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
очень долго и при этом легко допустить ошибку, поэтому такой способ не идеален. Если необходимо гарантировать, что поток завершается до выхода из функции потому ли, что он хранит ссылки на локальные переменные, или по какой-то иной причине то важно обеспечить это на всех возможных путях выхода, как нормальных, так и в результате исключения, и хотелось бы иметь для этого простой и лаконичный механизм.
Один из способов решить эту задачу воспользоваться стандартной идиомой 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
.