1.4.1. Здравствуй, параллельный мир
Начнем с классического примера — программы, которая печатает фразу «Здравствуй, мир». Ниже приведена тривиальная однопоточная программа такого сорта, от нее мы будем отталкиваться при переходе к нескольким потокам.
#include <iostream>
int main() {
std::cout << "Здравствуй, мир ";
}
Эта программа всего лишь выводит строку Здравствуй мир в стандартный поток вывода. Сравним ее с простой программой «Здравствуй, параллельный мир», показанной в листинге 1.1, — в ней для вывода сообщения запускается отдельный поток.
#include <iostream>
#include <thread> ← (1)
void hello() ← (2)
{
std::cout << "Здравствуй, параллельный мир ";
}
int
main() {
std::thread t(hello); ← (3)
t.join(); ← (4)
}
Прежде всего, отметим наличие дополнительной директивы #include <thread> (1). Все объявления, необходимые для поддержки многопоточности, помещены в новые заголовочные файлы; функции и классы для управления потоками объявлены в файле <thread>, а те, что нужны для защиты разделяемых данных, — в других заголовках.
Далее, код вывода сообщения перемещен в отдельную функцию (2). Это объясняется тем, что в каждом потоке должна быть начальная функция, в которой начинается исполнение потока. Для первого потока в приложении таковой является main(), а для всех остальных задается в конструкторе объекта std::thread. В данном случае в качестве начальной функции объекта типа std::thread, названного t (3), выступает функция hello().
Есть и еще одно отличие вместо того, чтобы сразу писать на стандартный вывод или вызывать hello() из main(), эта программа запускает новый поток, так что теперь общее число потоков равно двум: главный, с начальной функцией main(), и дополнительный, начинающий работу в функции hello().
После запуска нового потока (3) начальный поток продолжает работать. Если бы он не ждал завершения нового потока, то просто дошел бы до конца main(), после чего исполнение программы закончилась бы быть может, еще до того, как у нового потока появился шанс начать работу. Чтобы предотвратить такое развитие событие, мы добавили обращение к функции join() (4); в главе 2 объясняется, что это заставляет вызывающий поток (main()) ждать завершения потока, ассоциированного с объектом std::thread, — в данном случае t.
Если вам показалось, что для элементарного вывода сообщения на стандартный вывод работы слишком много, то так оно и есть, — в разделе 1.2.3 выше мы говорили, что обычно для решения такой простой задачи не имеет смысла создавать несколько потоков, особенно если главному потоку в это время нечего делать. Но далее мы встретимся с примерами, когда запуск нескольких потоков дает очевидный выигрыш.