Created by drewxa@
std::thread
std::jthread
std::async
std::future
Говоря о многозадачности, мы имеем ввиду, что несколько задач выполняются одновременно.
Многозада́чность — это, в первую очередь, свойство операционной системы или среды выполнения.
Оборудование с одноядерными процессорами не способно выполнять одновременно несколько задач. Но они могут создавать иллюзию этого.
Оборудование с несколькими процессорами (или несколькими ядрами на одном процессоре) может выполнять несколько задач одновременно. Это называется аппаратным параллелизмом.
Существует два типа многозадачности:
Далее в лекции будем говорить о поточной многозадачности.
Первая причина для использования многопоточное программирование – это разделение обязанностей.
Например, пользовательский интерфейс зачастую выполняется в отдельном потоке, в то время как основная логика ПО выполняется в других потоках.
Другая причина использования многопоточное программирование – повышение вычислительной производительности.
Сейчас существуют персональные компьютеры с 16 и более ядрами (не говоря уже о серверном оборудовании). При таком раскладе использование только одного потока для выполнения всех задач является ошибкой.
Самые распространненые применения многопоточности:
Многопоточность ради повышения вычислительной производительности приложения встречается не так часто, как асинхронное выполнение IO операций.
Однако, иногда, использование нескольких потоков позволяет значительно повысить производительность приложения.
Чтобы операционная система поддерживала многозадачность, каждый выполняемый поток должен обладать своим контекстом исполнения.
Этот контекст используется для хранения данных о текущем состоянии потока: значения регистров процессора, указателя на стек данных, указатель на текущую выполняемую команду.
В системе количество потоков может превышать (а на самом деле, почти всегда превышает) число ядер. И чтобы задачи могли корректно исполняться применяться механизм переключения между ними.
ОС передает поток на исполнение ядру процессора.
Этот поток исполняется в течение некоторого временного интервала.
После завершения этого интервала контекст ОС переключается на другой поток.
В стандарте C++11 появились классы для управления потоками, синхронизации операций между потоками и низкоуровневыми атомартными операциями.
В качестве основы для библиотек по работе с многопоточностью в стандарте были взяты аналоги из библиотеки Boost.
Использование любых высокоуровневых механизмов вместо низкоуровневых средств влечет за собой некоторые издержки. Их называют платой за абстрагирование.
Одной из целей, которую преследовали при проектировании библиотеки многопоточности, была минимизация платы за абстрагирование.
#include <iostream>
#include <thread>
void hello() {
std::cout << "Hello, World!";
}
int main() {
std::thread th(hello);
th.join();
}
std::thread
Создание объекта типа std::thread
запускает новый поток.
std::thread
До вызова деструктора объекта типа std::thread
необходимо вызвать или метод join()
, или метод detach()
.
Иначе, во время вызова деструктора произойдет вызов std::terminate()
.
std::thread::join
Вызов метода join
приведет к ожиданию завершения потока.
Это значит, что до тех пор пока поток не завершит своё выполнение, основной поток не будет выполнять код находящийся после вызова метода join()
.
std::thread::join
Этот метод необходимо использовать, если основному потоку необходим и важен результат выполнения дочернего потока.
Например, когда необходимо дождаться загрузки данных для дальнейшей обработки этих данных.
std::thread::detach
Вызов функции detach оставляет поток работать в фоновом режиме.
Это значит, что код находящийся после вызова метода detach()
может выполняться пока выполняется запущенный поток.
std::thread::detach
Этот метод необходимо использовать, если основному потоку не важен результат выполнения дочернего потока. Например, отправка пользовательской статистики.
std::jthread
std::jthread
обладает аналогичным поведением как и std::thread
с двумя особенностями:
std::jthread
Объекты типа std::jthread
содержат поле std::stop_source
– некоторое совместное состояние, которое используется для остановки потока.
std::jthread th([](std::stop_token stoken) {
int counter{0};
while (counter < 10){
std::this_thread::sleep_for(0.2s);
if (stoken.stop_requested()) return;
std::cerr << counter << std::endl;
++counter;
}
});
std::this_thread::sleep_for(1s);
th.request_stop();
Объекты типа std::stop_source
связаны с соответствующим std::stop_token
.
Если у объекта std::stop_source
вызывать метод request_stop
, но меняется внутренее состояние объекта.
После этого у связанного std::stop_token
метод stop_requested
будет возвращать true
.
Объекты std::jthread
содержат поле типа std::stop_source
.
В конструкторе std::jthread
происходит связывание поля std::stop_source
с первым аргументом выполняемой фукнции.
Поэтому, если вы хотите останавливать поток используя описанный механизм, то std::stop_token
должен быть первым аргументом.
Класс std::thread
и std::jthread
являются перемещающимся типами со всеми вытекающими последствиями:
std::thread
/std::jthread
контейнерахstd::async
Своеобразными “конкурентами” классу std::thread
являются функция std::async
и класс future<T>
.
std::async
#include <iostream>
#include <thread>
std::string hello() {
return "Hello, World!";
}
int main() {
std::future<std::string> res =
std::async(hello);
std::cout << res.get();
}
std::future<T>
Функция std::async
возвращет объект класса future<T>
, который предоставляет доступ к результату выполнения потока: возвращаемому значению или исключению.
std::future<T>::get()
При вызове функции std::future<T>::get()
может произойти одно из трех событий:
async
в отдельном потоке и уже закончилось, то результат получится немедленно.std::future<T>::get()
async
в отдельном потоке, но еще не закончилось, то функция get() блокирует основной поток до получения результата.std::future<T>::get()
Если выполнение фоновой задачи было завершено из-за исключения, которое не было обработано в потоке, это исключение сгенерируется снова при попытке получить результат выполения потока, т.е. при вызове метода std::future<T>::get()
auto f = std::async([](){
throw 42;
});
try {
f.get();
} catch(int i) {
std::cout << i;
}
std::shared_future
Класс std::future
позволяет обрабатывать результат параллельных вычислений. Однако этот результат можно обрабатывать только один раз. Второй вызов функции std::future::get
приводит к неопределенному поведению.
std::shared_future
Но иногда приходится обрабатывать результат вычислений несколько раз, особенно если этот результат обрабатывают несколько других потоков. Для этой цели существует std::shared_future
std::shared_future
Объекты типа std::shared_future
допускают несколько вызовов метода get
, возвращает один и тот же результат или генерирует одно и то же исключение.
thread_local
std::stop_callback