std::thread my_thread(f);
В данном случае переданный объект-функция копируется в память, принадлежащую только что созданному потоку выполнения, и оттуда вызывается. Поэтому необходимо, чтобы с точки зрения поведения копия была эквивалентна оригиналу, иначе можно получить неожиданный результат.
При передаче объекта-функции конструктору потока нужно избегать феномена «самого досадного разбора в С++» (C++'s most vexing parse). Синтаксически передача конструктору временного объекта вместо именованной переменной выглядит так же, как объявление функции, и именно так компилятор и интерпретирует эту конструкцию. Например, в предложении
std::thread my_thread(background_task());
объявлена функция my_thread
, принимающая единственный параметр (типа указателя на функцию без параметров, которая возвращает объект background_task
) и возвращающая объект std::thread
. Никакой новый поток здесь не запускается. Решить эту проблему можно тремя способами: поименовать объект-функцию, как в примере выше; добавить лишнюю пару скобок или воспользоваться новым универсальным синтаксисом инициализации, например:
std::thread my_thread((background_task())); ←
(1)
std::thread my_thread{background_task()}; ←
(2)
В случае (1) наличие дополнительных скобок не дает компилятору интерпретировать конструкцию как объявление функции, так что действительно объявляется переменная my_thread
типа std::thread
. В случае (2) использован новый универсальный синтаксис инициализации с фигурными, а не круглыми скобками, он тоже приводит к объявлению переменной.
В стандарте С++11 имеется новый тип допускающего вызов объекта, в котором описанная проблема не возникает, — лямбда-выражение. Этот механизм позволяет написать локальную функцию, которая может захватывать некоторые локальные переменные, из-за чего передавать дополнительные аргументы просто не нужно (см. раздел 2.2). Подробная информация о лямбда-выражениях приведена в разделе А.5 приложения А. С помощью лямбда-выражений предыдущий пример можно записать в таком виде:
std::thread my_thread([](
do_something();
do_something_else();
});
После запуска потока необходимо явно решить, ждать его завершения (присоединившись к нему, см. раздел 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 ждем завершения