Created by drewxa@
std::threadstd::jthreadstd::asyncstd::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::jthreadstd::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_localstd::stop_callback