C++17 STL Стандартная библиотека шаблонов - Яцек Галовиц
Шрифт:
Интервал:
Закладка:
#include <iostream>
#include <thread>
using namespace std;
using namespace chrono_literals;
2. Чтобы запустить поток, следует указать, какой код он должен выполнить. Поэтому определим функцию, которую ему нужно выполнить. Функции — естественные потенциальные входные точки для потоков. Наша функция-пример принимает аргумент i, выступающий в роли идентификатора потока. Таким образом, можно сказать, какой поток отобразил то или иное сообщение. Кроме того, воспользуемся идентификатором потока с целью указать, что различные потоки нужно приостановить на разные промежутки времени. Это позволит убедиться в том, что они не пытаются задействовать команду cout одновременно. Если такая ситуация произойдет, то выходные данные будут искажены. Другой пример в настоящей главе посвящен именно этой проблеме.
static void thread_with_param(int i)
{
this_thread::sleep_for(1ms * i);
cout << "Hello from thread " << i << 'n';
this_thread::sleep_for(1s * i);
cout << "Bye from thread " << i << 'n';
}
3. В функции main (просто из любопытства) выведем на экран информацию о том, сколько потоков можно запустить в одно время с помощью std::thread::hardware_concurrency. Данное значение зависит от того, сколько ядер имеет процессор и сколько ядер поддерживается реализацией STL. Это говорит о том, что значения будут различаться для разных компьютеров.
int main()
{
cout << thread::hardware_concurrency()
<< " concurrent threads are supported.n";
4. Наконец, начнем работать с потоками. Запустим три потока с разными идентификаторами. При создании экземпляра потока с помощью выражения наподобие t{f, x} получаем вызов функции f(x). Таким образом можно передавать функциям thread_with_param разные аргументы для каждого потока:
thread t1 {thread_with_param, 1};
thread t2 {thread_with_param, 2};
thread t3 {thread_with_param, 3};
5. Поскольку данные потоки запущены свободно, нужно остановить их, когда они закончат выполнять свою работу. Сделаем это с помощью функции join. Она заблокирует вызов потока до тех пор, пока вызываемый поток не отработает:
t1.join();
t2.join();
6. Альтернативой присоединению является открепление. Если мы не вызовем функцию join или detach, то все приложение завершится довольно шумно, как только будет выполнен деструктор объекта потока. Путем вызова функции detach указываем thread, что хотим продолжения работы потока номер 3 даже после того, как его экземпляр будет разрушен:
t3.detach();
7. Перед завершением функции main и всей программы выведем еще одно сообщение:
cout << "Threads joined.n";
}
8. Компиляция и запуск программы дадут следующий результат. Мы можем увидеть, что моя машина имеет восемь ядер процессора. Далее сообщения hello видим из всех потоков, а сообщения bye — лишь из двух, которые мы объединили. Поток 3 все еще ожидает завершения трехсекундного ожидания, но вся программа уже завершилась после того, как поток 2 завершил свое двухсекундное ожидание. Таким образом, мы не можем увидеть прощальное сообщение потока 3, поскольку он был уничтожен:
$ ./threads
8 concurrent threads are supported.
Hello from thread 1
Hello from thread 2
Hello from thread 3
Bye from thread 1
Bye from thread 2
Threads joined.
Как это работает
Запуск и остановку потоков выполнить очень просто. Многопроцессорная обработка начинает усложняться в момент, когда потокам нужно работать вместе (делить ресурсы, ожидать завершения других потоков и т.д.).
Чтобы запустить поток, нужно иметь функцию, которую он будет выполнять. Функция не обязательно должна быть особенной, поскольку в потоке можно выполнить практически любую функцию. Напишем небольшую программу-пример, которая запускает поток и ожидает его завершения:
void f(int i) { cout << i << 'n'; }
int main()
{
thread t {f, 123};
t.join();
}
Вызов конструктора std::thread принимает указатель на функцию или вызываемый объект; за ним следуют аргументы, которые нужно использовать в вызове функции. Конечно, можете также запустить поток или функцию, не принимающие никаких параметров.
При наличии в системе нескольких ядер процессора потоки можно выполнять параллельно и конкурентно. В чем заключается разница? Если компьютер имеет всего одно ядро ЦП, то можно создать множество потоков, работающих параллельно, но не конкурентно, поскольку ядро способно запускать лишь один поток в любой момент времени. Потоки запускаются и чередуются, где каждый поток выполняется какую-то часть секунды, затем приостанавливается, после чего следующий поток получает время (для пользователей-людей кажется, что потоки выполняются одновременно). Если потокам не нужно делить одно ядро, то они могут быть запущены конкурентно и действительно работать одновременно.
К этому моменту мы не контролируем следующие детали:
□ порядок, в котором потоки чередуются на одном ядре;
□ приоритет потока, указывающий, что один поток главнее другого;
□ распределение потоков между ядрами. Вполне возможна ситуация, когда все потоки будут выполняться на одном ядре, несмотря на то что машина имеет более 100 ядер.
Большая часть операционных систем предоставляет возможности управления этими аспектами многопроцессорной обработки, но на текущий момент данные функции не включены в STL.
Однако можно запускать и останавливать потоки и указывать им, когда и над чем работать и когда останавливаться. Этого должно быть достаточно для большинства приложений. В данном разделе мы создали три дополнительных потока. После этого объединили большую их часть и открепили последний. Подытожим на одном рисунке все, что произошло (рис. 9.1).
Читая рисунок сверху вниз, мы заметим, что в какой-то момент разбиваем рабочий поток программы на четыре потока. Мы запускаем три дополнительных потока, которые совершают некие действия (а именно, ожидают и выводят сообщения), но после их запуска основной поток, выполняющий функцию main, остается без работы.
Когда поток завершает выполнение своей функции, он возвращает значение, возвращенное ею. Затем стандартная библиотека «делает уборку», что приводит к удалению потока из планировщика системы и, возможно, его уничтожению, но волноваться об этом не нужно.
Единственное, о чем следует волноваться, — это объединение. Когда поток вызывает функцию x.join() для объекта другого потока, его выполнение приостанавливается до того, как будет выполнен поток x. Обратите внимание: нас ничто не спасет при попадании потока в бесконечный цикл! Если нужно, чтобы поток продолжал существовать до тех пор, пока не решит завершиться,