Лекция 4

Управление потоками

Created by drewxa@

Содержание

  • многозадачность в ПО
  • переключение контекста потоков
  • std::thread
  • std::jthread
  • std::async
  • std::future

Многозадачность

Говоря о многозадачности, мы имеем ввиду, что несколько задач выполняются одновременно.

Многозада́чность — это, в первую очередь, свойство операционной системы или среды выполнения.

Многозадачность

Оборудование с одноядерными процессорами не способно выполнять одновременно несколько задач. Но они могут создавать иллюзию этого.

Аппаратный параллелизм

Оборудование с несколькими процессорами (или несколькими ядрами на одном процессоре) может выполнять несколько задач одновременно. Это называется аппаратным параллелизмом.

Типы многозадачности

Существует два типа многозадачности:

  • процессная многозадачность
  • поточная многозадачность

Далее в лекции будем говорить о поточной многозадачности.

Причины использовать многозадачность

Первая причина для использования многопоточное программирование – это разделение обязанностей.

Например, пользовательский интерфейс зачастую выполняется в отдельном потоке, в то время как основная логика ПО выполняется в других потоках.

Причины использовать многозадачность

Другая причина использования многопоточное программирование – повышение вычислительной производительности.

Сейчас существуют персональные компьютеры с 16 и более ядрами (не говоря уже о серверном оборудовании). При таком раскладе использование только одного потока для выполнения всех задач является ошибкой.

content

Разделение обязанностей

Самые распространненые применения многопоточности:

  • отзывчивый интерфейс приложения
  • запуск длительнных операций в отдельном потоке (например, сетевые запросы, чтение и запись на диск)

Повышение производительности

Многопоточность ради повышения вычислительной производительности приложения встречается не так часто, как асинхронное выполнение IO операций.

Однако, иногда, использование нескольких потоков позволяет значительно повысить производительность приложения.

Контекст потоков

Чтобы операционная система поддерживала многозадачность, каждый выполняемый поток должен обладать своим контекстом исполнения.

Контекст потоков

Этот контекст используется для хранения данных о текущем состоянии потока: значения регистров процессора, указателя на стек данных, указатель на текущую выполняемую команду.

Многозадачность в ОС

В системе количество потоков может превышать (а на самом деле, почти всегда превышает) число ядер. И чтобы задачи могли корректно исполняться применяться механизм переключения между ними.

Многозадачность на одном ядре

ОС передает поток на исполнение ядру процессора.

Этот поток исполняется в течение некоторого временного интервала.

После завершения этого интервала контекст ОС переключается на другой поток.

Переключение контекста

  • обновляется контекст текущего потока
  • из имеющихся потоков в ОС выбирается один, который будет исполняться на процессоре
  • загружается контекст выбранного потока

Переключение контекста

content

C++11

В стандарте 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 с двумя особенностями:

  • в своем деструкторе автоматически вызывает метод join
  • можно остановить выполение потока, если предусмотреть такую возможность

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.

Взаимоотношения jthread, stop_source, stop_token

Объекты 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() может произойти одно из трех событий:

  1. Если выполнение асинхронной функции было начато функцией async в отдельном потоке и уже закончилось, то результат получится немедленно.

std::future<T>::get()

  1. Если выполнение асинхронной функции было начато функцией async в отдельном потоке, но еще не закончилось, то функция get() блокирует основной поток до получения результата.

std::future<T>::get()

  1. Если выполнение целевой функции еще не начиналось, то она начнет выполняться как обычная синхронная функция.

Исключения в потоках

Если выполнение фоновой задачи было завершено из-за исключения, которое не было обработано в потоке, это исключение сгенерируется снова при попытке получить результат выполения потока, т.е. при вызове метода 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