Привет, Хабр! Перевод статьи подготовлен в рамках курса "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.
thatsme
Из за того что в C++ до сих пор нет cancelable threads, приходится использовать обёртки над pthread. С threads в C++ целая куча проблем. Например невозможность корректно завершить програму если один из потоков ожидает ввода-вывода или семафора, ну или ожидает выполнения любой из функций являющихся cancellation point. В принципе это не проблема, но хотелось-бы подобные вещи иметь из коробки.
Но в любом случае, установка хэндлера корректной очистки при cancellation, с помощью pthread_cleanup_push и pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, ...) и pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, ...) в обёртке, всё равно лучше чем опрос is_interrupted по каждому чиху (и об этом опросе можно ведь забыть, т.е. и тут придётся обёртку делать).