Привет, Хабр! Перевод статьи подготовлен в рамках курса "C++ Developer. Professional"
Один из участников моего семинара
в рамках CppCon 2018 спросил меня: Может ли
std::thread
быть прерван (interrupted)?. Мой ответ
тогда был нет, но это уже не совсем так. С C++20 мы можем получить
std::jthread
(в итоге все таки получили прим.
переводчика).
Позвольте мне развить тему, поднятую на CppCon 2018. Во время перерыва в моем семинаре, посвященному параллелизму, я побеседовал с Николаем (Йосуттисом). Он спросил меня, что я думаю о новом предложении P0660: Cooperatively Interruptible Joining Thread. На тот момент я ничего не знал об этом предложении. Следует отметить, что Николай является одним из авторов этого предложения (наряду с Хербом Саттером и Энтони Уильямсом). Сегодняшняя статья посвящена будущему параллелизма в C++. Ниже я привел общую картину параллелизма в текущем и грядущем C++.
Из названия документа
Cooperatively Interruptible Joining Thread (совместно
прерываемый присоединяемый поток) вы можете догадаться, что новый
поток имеет две новые возможности: прерываемость (interruptible) и
автоматическое присоединение (automatically joining, здесь и далее
присоединение блокировка вызывающего потока до завершения
выполнения, результат вызова метода join()
прим.
переводчика). Позвольте мне сначала рассказать вам об
автоматическом присоединении.
Автоматическое присоединение
Это неинтуитивное поведение std::thread
. Если
std::thread
все еще является joinable
, то
в его деструкторе вызывается std::terminate
. Поток
thr
является joinable
, если ни
thr.join()
, ни thr.detach()
еще не были
вызваны.
// threadJoinable.cpp#include <iostream>#include <thread>int main(){ std::cout << std::endl; std::cout << std::boolalpha; std::thread thr{[]{ std::cout << "Joinable std::thread" << std::endl; }}; std::cout << "thr.joinable(): " << thr.joinable() << std::endl; std::cout << std::endl; }
При выполнении программа терминируется.
Оба потока терминируются. На втором запуске поток
th
имеет достаточно времени, чтобы отобразить свое
сообщение: Joinable std::thread
.
В следующем примере я заменяю хедер <thread>
на "jthread.hpp
" и использую std::jthread
из грядущего стандарта C++.
// jthreadJoinable.cpp#include <iostream>#include "jthread.hpp"int main(){ std::cout << std::endl; std::cout << std::boolalpha; std::jthread thr{[]{ std::cout << "Joinable std::thread" << std::endl; }}; std::cout << "thr.joinable(): " << thr.joinable() << std::endl; std::cout << std::endl; }
Теперь поток thr
автоматически присоединяется в
своем деструкторе, если он все еще является joinable
,
как, например, в этом примере.
Прерывание std::jthread
Чтобы было от чего отталкиваться, позвольте мне продемонстрировать вам простой пример.
// interruptJthread.cpp#include "jthread.hpp"#include <chrono>#include <iostream>using namespace::std::literals;int main(){ std::cout << std::endl; std::jthread nonInterruptable([]{ // (1) int counter{0}; while (counter < 10){ std::this_thread::sleep_for(0.2s); std::cerr << "nonInterruptable: " << counter << std::endl; ++counter; } }); std::jthread interruptable([](std::interrupt_token itoken){ // (2) int counter{0}; while (counter < 10){ std::this_thread::sleep_for(0.2s); if (itoken.is_interrupted()) return; // (3) std::cerr << "interruptable: " << counter << std::endl; ++counter; } }); std::this_thread::sleep_for(1s); std::cerr << std::endl; std::cerr << "Main thread interrupts both jthreads" << std:: endl; nonInterruptable.interrupt(); interruptable.interrupt(); // (4) std::cout << std::endl; }
Я запустил в main два потока, nonInterruptable
,
который нельзя прерывать, и interruptable
, который
можно (строки 1 и 2). В отличие от потока
nonInterruptable
, поток interruptable
,
получает std::interrupt_token
и использует его в
строке 3, чтобы проверить, был ли он прерван:
itoken.is_interrupted()
. В случае прерывания в лямбде
срабатывает return
и, следовательно, поток
завершается. Вызов interruptable.interrupt()
(строка
4) триггерит завершение потока. Аналогичный вызов
nonInterruptable.interrupt()
не сработает для потока
nonInterruptable
, который, как мы видим, продолжает
свое выполнение.
Вот более подробная информация о токенах прерывания (interrupt tokens), присоединяющихся потоках и условных переменных.
Токены прерывания
Токен прерывания std::interrupt_token
моделирует
совместное владение (shared ownership) и может использоваться для
сигнализирования о прерывании, если токен валиден. Он предоставляет
три метода: valid
, is_interrupted
, и
interrupt
.
itoken.valid() true
, если токен прерывания может
быть использован для сигнализировании о прерывании
itoken.is_interrupted() true
, если был
инициализирован с true
или был вызван метода
interrupt()
itoken.interrupt()
если !valid()
или
is_interrupted()
, то вызов метода не возымеет эффекта.
В противном случае, сигнализирует о прерывании посредством
itoken.is_interrupted() == true
. Возвращает значение
is_interrupted()
Если токен прерывания должен быть временно отключен, вы можете заменить его дефолтным токеном. Дефолтный токен не валиден. Следующий фрагмент кода демонстрирует, как включать и отключать возможность потока принимать сигналы.
std::jthread jthr([](std::interrupt_token itoken){ ... std::interrupt_token interruptDisabled; std::swap(itoken, interruptDisabled); // (1) ... std::swap(itoken, interruptDisabled); // (2) ...}
std::interrupt_token interruptDisabled
не валиден.
Это означает, что поток не может принять прерывание между строками
(1) и (2), но после строки (2) уже может.
Присоединение потоков
std::jhread
представляет собой
std::thread
с дополнительным функционалом, реализующим
сигнализирование о прерывании и автоматическое присоединение. Для
поддержки этой функциональности у него есть
std::interrupt_token
.
Новые перегрузки Wait для условных переменных
Две вариации wait wait_for
и
wait_until
из std::condition_variable
получат новые перегрузки. Они принимают
std::interrupt_token
.
template <class Predicate>bool wait_until(unique_lock<mutex>& lock, Predicate pred, interrupt_token itoken);template <class Rep, class Period, class Predicate>bool wait_for(unique_lock<mutex>& lock, const chrono::duration<Rep, Period>& rel_time, Predicate pred, interrupt_token itoken);template <class Clock, class Duration, class Predicate>bool wait_until(unique_lock<mutex>& lock, const chrono::time_point<Clock, Duration>& abs_time, Predicate pred, interrupt_token itoken);
Новые перегрузки требует предикат. Эти версии гарантированно
получают уведомления, если поступает сигнал о прерывании для
переданного им std::interrupt_token itoken
. После
вызовов wait
вы можете проверить, не произошло ли
прерывание.
cv.wait_until(lock, predicate, itoken);if (itoken.is_interrupted()){ // interrupt occurred}
Что дальше?
Как я и обещал в своей последней статье, следующая статья будет посвящена оставшимся правилам определения концептов (concepts).
Узнать подробнее о курсе "C++ Developer. Professional".
Смотреть запись демо-занятия по теме Области видимости и невидимости: участники вместе с экспертом попробовали реализовать класс общего назначения и запустить несколько unit-тестов с использованием googletest.