После запуска потока необходимо явно решить, ждать его завершения (присоединившись к нему, см. раздел 2.1.2) или предоставить собственной судьбе (отсоединив его, см. раздел 2.1.3). Если это решение не будет принято к моменту уничтожения объекта std::thread
, то программа завершится (деструктор std::thread
вызовет функцию std::terminate()
). Поэтому вы обязаны гарантировать, что поток корректно присоединен либо отсоединен, даже если возможны исключения. Соответствующая техника программирования описана в разделе 2.1.3. Отметим, что это решение следует принять именно до уничтожения объекта std::thread
, к самому потоку оно не имеет отношения. Поток вполне может завершиться задолго до того, как программа присоединится к нему или отсоединит его. А отсоединенный поток может продолжать работу и после уничтожения объекта std::thread
.
Если вы не хотите дожидаться завершения потока, то должны гарантировать, что данные, к которым поток обращается, остаются действительными до тех пор, пока они могут ему понадобиться. Эта проблема не нова даже в однопоточной программа доступ к уже уничтоженному объекту считается неопределенным поведением, но при использовании потоков есть больше шансов столкнуться с проблемами, обусловленными временем жизни.
Например, такая проблема возникает, если функция потока хранит указатели или ссылки на локальные переменные, и поток еще не завершился, когда произошел выход из области видимости, где эти переменные определены. Соответствующий пример приведен в листинге 2.1.
Листинг 2.1. Функция возвращает управление, когда поток имеет доступ к определенным в ней локальным переменным
struct func {
int& i;
func(int& i_) : i(i_){}
void operator() () {
for(unsigned j = 0; j < 1000000; ++j) {
do_something(i); ←┐
Потенциальный доступ
}
(1) к висячей ссылке
}
};
void oops() {
int some_local_state = 0;
(2) He ждем завершения
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. Ожидание завершения потока
Чтобы дождаться завершения потока, следует вызвать функцию join()
ассоциированного объекта std::thread
. В листинге 2.1 мы можем заменить вызов my_thread.detach()
перед закрывающей скобкой тела функции вызовом my_thread.join()
, и тем самым гарантировать, что поток завершится до выхода из функции, то есть раньше, чем будут уничтожены локальные переменные. В данном случае это означает, что запускать функцию в отдельном потоке не имело смысла, так как первый поток в это время ничего не делает, по в реальной программе исходный поток мог бы либо сам делать что-то полезное, либо запустить несколько потоков параллельно, а потом дождаться их всех.