Потоки: зачем и как
Чему вы научитесь:
- запускать функцию в отдельном потоке и дожидаться его через
join(); - объяснять, что такое гонка данных, и защищать общие данные мьютексом;
- выбирать между мьютексом и
std::atomic; - забирать результат из другого потока через
std::asyncи считать время типами из<chrono>.

Представьте кухню ресторана. Повар готовит суп по карточке рецепта: шаг за шагом, по одной строке. Пока он режет овощи, салат никто не делает, хотя рядом есть свободное место у стола. Второй повар мог бы встать туда и делать салат одновременно с супом. У каждого повара своя карточка, а холодильник у них общий.
Программа устроена так же. Она выполняет инструкции по одной: пока функция считает, остальная работа ждёт. При этом в процессоре обычно несколько ядер — это рабочие места у стола. Каждое ядро выполняет свою последовательность инструкций одновременно с остальными.
Поток — это повар со своей карточкой: последовательность инструкций, которая выполняется независимо от других. У каждой программы есть хотя бы один поток — тот, что выполняет main. Чтобы две задачи шли одновременно, нужен второй поток. Его запускает объект std::thread из заголовка <thread>:
#include <iostream>
#include <thread>
void print_squares() {
for (int i = 1; i <= 3; ++i) {
std::cout << i * i << ' '; // 1 4 9
}
}
int main() {
std::thread worker(print_squares);
worker.join();
std::cout << "done\n"; // done
}Здесь аналогия с местами у стола заканчивается: потоков можно запустить больше, чем ядер. Тогда ядро быстро переключается между потоками, и снаружи их работа всё равно выглядит одновременной.
Запуск и ожидание
Давайте разберём main по строкам:
std::thread worker(print_squares);создаёт поток и сразу запускает в нёмprint_squares. Функцияmainпри этом не ждёт: следующая строка выполняется в главном потоке одновременно с новым.worker.join();останавливаетmain, пока потокworkerне закончит работу. Так главный повар ждёт, пока второй доделает салат.std::cout << "done\n";выполняется только после этого, поэтомуdoneвсегда печатается после квадратов.
Если бы main печатала done до join(), вывод двух потоков мог бы перемешаться. Порядок, в котором потоки выполняют свои инструкции, заранее неизвестен.
Аргументы и общая память
Аргументы для функции передают в конструктор после её имени: std::thread t(f, 1, 2) вызовет в новом потоке f(1, 2). Вместо функции можно передать и лямбду.
Все потоки программы работают с общей памятью, как повара с общим холодильником: глобальная переменная у них одна на всех.
В задачах курса потоков нет: код с std::thread или std::async там не заработает, поэтому примеры этой главы запускайте на своём компьютере.
detach и jthread
Вместо join() можно вызвать detach(): поток продолжит работать сам по себе, но дождаться его будет уже нельзя. Это рискованно. Если такой поток читает локальные переменные функции, которая его запустила, он может обратиться к ним после их уничтожения. Это неопределённое поведение.
В C++20 появился std::jthread. Запускают его так же, как std::thread, но деструктор std::jthread сам вызывает join(). Поэтому забыть дождаться такого потока нельзя.
Частая ошибка
Поток запустили, но не дождались:
int main() {
std::thread worker(print_squares);
std::cout << "done\n";
} // ✗terminate called without an active exceptionПрограмма собирается без предупреждений. Но при выходе из main объект worker уничтожается, хотя для него не вызван ни join(), ни detach(). В таком случае деструктор std::thread вызывает std::terminate, и программа завершается аварийно. Вызовите join() до конца области видимости или используйте std::jthread.
Главное
- Поток — последовательность инструкций, которая выполняется одновременно с другими;
std::threadзапускает функцию в новом потоке. join()ждёт, пока поток закончит работу; порядок инструкций разных потоков заранее неизвестен.- Все потоки программы работают с одной общей памятью.
- Объект
std::threadбезjoin()илиdetach()при уничтожении вызываетstd::terminate;std::jthreadвызываетjoin()сам.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.