Листинг 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
очень долго и при этом легко допустить ошибку, поэтому такой способ не идеален. Если необходимо гарантировать, что поток завершается до выхода из функции потому ли, что он хранит ссылки на локальные переменные, или по какой-то иной причине то важно обеспечить это на всех возможных путях выхода, как нормальных, так и в результате исключения, и хотелось бы иметь для этого простой и лаконичный механизм.
Один из способов решить эту задачу воспользоваться стандартной идиомой захват ресурса есть инициализация (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()
. Отсоединенные потоки действительно работают в фоне: отныне ими владеет и управляет библиотека времени выполнения С++, которая обеспечит корректное освобождение связанных с потоком ресурсов при его завершении.
Отсоединенные потоки часто называют потоками-демонами по аналогии с процессами-демонами в UNIX, то есть с процессами, работающими в фоновом режиме и не имеющими явного интерфейса с пользователем. Обычно такие потоки работают в течение длительного времени, в том числе на протяжении всего времени жизни приложения. Они, например, могут следить за состоянием файловой системы, удалять неиспользуемые записи из кэша или оптимизировать структуры данных. С другой стороны, иногда отсоединенный поток применяется, когда существует какой-то другой способ узнать о его завершении или в случае, когда нужно запустить задачу и «забыть» о ней.
В разделе 2.1.2 мы уже видели, что для отсоединения потока следует вызвать функцию-член detach()
объекта std::thread
. После возврата из этой функции объект std::thread
уже не связан ни с каким потоком, и потому присоединиться к нему невозможно.
std::thread t(do_background_work);
t.detach();
assert(!t.joinable());
Разумеется, чтобы отсоединить поток от объекта std::thread
, поток должен существовать: нельзя вызвать detach()
для объекта std::thread
, с которым не связан никакой поток. Это то же самое требование, которое предъявляется к функции join()
, поэтому и проверяется оно точно так жевызывать t.detach()
для объекта t
типа std::thread
можно только тогда, когда t.joinable()
возвращает true
.
Возьмем в качестве примера текстовый редактор, который умеет редактировать сразу несколько документов. Реализовать его можно разными способамикак на уровне пользовательского интерфейса, так и с точки зрения внутренней организации. В настоящее время все чаще для этой цели используют несколько окон верхнего уровня, по одному для каждого редактируемого документа. Хотя эти окна выглядят совершенно независимыми, в частности, у каждого есть свое меню и все прочее, на самом деле они существуют внутри единственного экземпляра приложения. Один из подходов к внутренней организации программы заключается в том, чтобы запускать каждое окно в отдельном потоке: каждый такой поток исполняет один и тот же код, но с разными данными, описывающими редактируемый документ и соответствующее ему окно. Таким образом, чтобы открыть еще один документ, необходимо создать новый поток. Потоку, обрабатывающему запрос, нет дела до того, когда созданный им поток завершится, потому что он работает над другим, независимым документом. Вот типичная ситуация, когда имеет смысл запускать отсоединенный поток.
В листинге 2.4 приведен набросок кода, реализующего этот подход.
Листинг 2.4. Отсоединение потока для обработки другого документа
void edit_document(std::string const& filename) {
open_document_and_display_gui(filename);
while(!done_editing()) {
user_command cmd = get_user_input();
if (cmd.type == open_new_document) {
std::string const new_name = get_filename_from_user();
std::thread t(edit_document,new_name);
(1)
t.detach();
(2)
}
else {
process_user_input(cmd);
}
}
}
Когда пользователь открывает новый документ, мы спрашиваем, какой документ открыть, затем запускаем поток, в котором этот документ открывается (1), и отсоединяем его (2). Поскольку новый поток делает то же самое, что текущий, только с другим файлом, то мы можем использовать ту же функцию (edit_document
), передав ей в качестве аргумента имя только что выбранного файла.
Этот пример демонстрирует также, почему бывает полезно передавать аргументы функции потока: мы передаем конструктору объекта std::thread
не только имя функции (1), но и её параметримя файла. Существуют другие способы добиться той же цели, например, использовать не обычную функцию с параметрами, а объект-функцию с данными-членами, но библиотека предлагает и такой простой механизм.
2.2. Передача аргументов функции потока
Из листинга 2.4 видно, что по существу передача аргументов вызываемому объекту или функции сводится просто к передаче дополнительных аргументов конструктору std::thread
. Однако важно иметь в виду, что по умолчанию эти аргументы копируются в память объекта, где они доступны вновь созданному потоку, причем так происходит даже в том случае, когда функция ожидает на месте соответствующего параметра ссылку. Вот простой пример:
void f(int i, std::string const& s);
std::thread t(f, 3, "hello");
Здесь создается новый ассоциированный с объектом t
поток, в котором вызывается функция f(3, "hello")
. Отметим, что функция f
принимает в качестве второго параметра объект типа std::string
, но мы передаем строковый литерал char const*
, который преобразуется к типу std::string
уже в контексте нового потока. Это особенно важно, когда переданный аргумент является указателем на автоматическую переменную, как в примере ниже:
void f(int i, std::string const& s);
void oops(int some_param) {
char buffer[1024];
(1)
sprintf(buffer, "%i", some_param);
std::thread t(f, 3, buffer);
(2)
t.detach();
}
В данном случае в новый поток передается (2) указатель на локальную переменную buffer
(1), и есть все шансы, что выход из функции oops произойдет раньше, чем буфер будет преобразован к типу std::string
в новом потоке. В таком случае мы получим неопределенное поведение. Решение заключается в том, чтобы выполнить преобразование в std::string
до передачи buffer
конструктору std::thread
:
void f(int i,std::string const& s);
void not_oops(int some_param) {
char buffer[1024];
Использование
sprintf(buffer, "%i", some_param);
std::string
std::thread t(f, 3, std::string(buffer));
позволяет избежать
t.detach();
висячего указателя
}
В данном случае проблема была в том, что мы положились на неявное преобразование указателя на buffer
к ожидаемому типу первого параметра std::string
, а конструктор std::thread
копирует переданные значения «как есть», без преобразования к ожидаемому типу аргумента.
Возможен и обратный сценарий: копируется весь объект, а вы хотели бы получить ссылку Такое бывает, когда поток обновляет структуру данных, переданную по ссылке, например:
void update_data_for_widget(widget_id w,widget_data& data);
(1)
void oops_again(widget_id w) {
widget_data data;
std::thread t(update_data_for_widget, w, data);
(2)
display_status();
t.join();
process_widget_data(data);
(3)
}
Здесь update_data_for_widget
(1) ожидает, что второй параметр будет передан по ссылке, но конструктор std::thread
(2) не знает об этом: он не в курсе того, каковы типы аргументов, ожидаемых функцией, и просто слепо копирует переданные значения. Поэтому функции update_data_for_widget
будет передана ссылка на внутреннюю копию data
, а не на сам объект data
. Следовательно, по завершении потока от обновлений ничего не останется, так как внутренние копии переданных аргументов уничтожаются, и функция process_widget_data
получит не обновленные данные, а исходный объект data
(3). Для читателя, знакомого с механизмом std::bind
, решение очевидно: нужно обернуть аргументы, которые должны быть ссылками, объектом std::ref
. В данном случае, если мы напишем
std::thread t(update_data_for_widget, w, std::ref(data));
то функции update_data_for_widget
будет правильно передана ссылка на data
, а не копия data.
Если вы знакомы с std::bind
, то семантика передачи параметров вряд ли вызовет удивление, потому что работа конструктора std::thread
и функции std::bind
определяется в терминах одного и того же механизма. Это, в частности, означает, что в качестве функции можно передавать указатель на функцию-член при условии, что в первом аргументе передается указатель на правильный объект:
class X {
public:
void do_lengthy_work();
};
X my_x;
std::thread t(&X::do_lengthy_work, &my_x);
(1)
Здесь мы вызываем my_x.do_lengthy_work()
в новом потоке, поскольку в качестве указателя на объект передан адрес my_x
(1). Так вызванной функции-члену можно передавать и аргументы: третий аргумент конструктора std::thread
станет первым аргументом функции-члена и т.д.
Еще один интересный сценарий возникает, когда передаваемые аргументы нельзя копировать, а можно только перемещать: данные, хранившиеся в одном объекте, переносятся в другой, а исходный объект остается «пустым». Примером может служить класс std::unique_ptr
, который обеспечивает автоматическое управление памятью для динамически выделенных объектов. В каждый момент времени на данный объект может указывать только один экземпляр std::unique_ptr
, и, когда этот экземпляр уничтожается, объект, на который он указывает, удаляется. Перемещающий конструктор и перемещающий оператор присваивания позволяют передавать владение объектом от одного экземпляра std::unique_ptr
другому (о семантике перемещения см. приложение А, раздел А.1.1). После такой передачи в исходном экземпляре остается указатель NULL. Подобное перемещение значений дает возможность передавать такие объекты в качестве параметров функций или возвращать из функций. Если исходный объект временный, то перемещение производится автоматически, а если это именованное значение, то передачу владения следует запрашивать явно, вызывая функцию std::move()
. В примере ниже показано применение функции std::move
для передачи владения динамическим объектом потоку:
void process_big_object(std::unique_ptr<big_object>);
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object,std::move(p));
Поскольку мы указали при вызове конструктора std::thread
функцию std::move
, то владение объектом big_object
передается объекту во внутренней памяти вновь созданного потока, а затем функции process_big_object
.
В стандартной библиотеке Thread Library есть несколько классов с такой же семантикой владения, как у std::unique_ptr
, и std::thread
один из них. Правда, экземпляры std::thread
не владеют динамическими объектами, как std::unique_ptr
, зато они владеют ресурсами: каждый экземпляр отвечает за управление потоком выполнения. Это владение можно передавать от одного экземпляра другому, поскольку экземпляры std::thread
перемещаемые, хотя и не копируемые. Тем самым гарантируется, что в каждый момент времени с данным потоком будет связан только один объект, но в то же время программист вправе передавать владение от одного объекта другому