func my_func(some_local_state); ←┘
потока
std::thread my_thread(my_func); ←┐
Новый поток, возможно,
my_thread.detach();
(3) еще работает
}
В данном случае вполне возможно, что новый поток, ассоциированный с объектом my_thread
, будет еще работать, когда функция oops
вернет управление (2), поскольку мы явно решили не дожидаться его завершения, вызвав detach()
(3). А если поток действительно работает, то при следующем вызове do_something(i)
(1) произойдет обращение к уже уничтоженной переменной. Точно так же происходит в обычном однопоточном коде — сохранять указатель или ссылку на локальную переменную после выхода из функции всегда плохо, — но в многопоточном коде такую ошибку сделать проще, потому что не сразу видно, что произошло.
Один из распространенных способов разрешить такую ситуацию — сделать функцию потока замкнутой, то есть копировать в поток данные, а не разделять их. Если функция потока реализовала в виде вызываемого объекта, то сам этот объект копируется в поток, поэтому исходный объект можно сразу же уничтожить. Однако по-прежнему необходимо следить за тем, чтобы объект не содержал ссылок или указателей, как в листинге 2.1. В частности, не стоит создавать внутри функции поток, имеющий доступ к локальным переменным этой функции, если нет гарантии, что поток завершится до выхода из функции.
Есть и другой способ — явно гарантировать, что поток завершит исполнение до выхода из функции, присоединившись к нему.
2.1.2. Ожидание завершения потока
Чтобы дождаться завершения потока, следует вызвать функцию join()
ассоциированного объекта std::thread
. В листинге 2.1 мы можем заменить вызов my_thread.detach()
перед закрывающей скобкой тела функции вызовом my_thread.join()
, и тем самым гарантировать, что поток завершится до выхода из функции, то есть раньше, чем будут уничтожены локальные переменные. В данном случае это означает, что запускать функцию в отдельном потоке не имело смысла, так как первый поток в это время ничего не делает, по в реальной программе исходный поток мог бы либо сам делать что-то полезное, либо запустить несколько потоков параллельно, а потом дождаться их всех.
Функция 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)