Борис Сазонов, RAII потоки и CancellationToken в C++
1. RAII потоки и cancellation_token в C++
Борис Сазонов, 2016
2. Disclaimer
В этой презентации вы встретите:
✓ Извращения в форматировании кода
✓ C-style комментарии
✓ Чересчур лаконичные имена классов
✓ Неэффективный код без использования move и forward
✓ Отсутствие проверки ошибок
✓ Невидимый “using namespace std”
✓ Прочие гадости
Всё это сделано для того, чтобы сохранить разумный размер шрифта. Всегда пожалуйста.
10. Спасение от зловещего деструктора: detach vs. join
void reset()
{ _impl.detach(); }
− Небезопасно - в любой непонятной
ситуации поток будет работать с
мёртвым объектом
− Нарушение RAII
− Личная неприязнь
class worker {
atomic<bool> _alive;
raii_thread _thread;
public:
worker() : _alive(true) {
_thread = raii_thread(bind(&worker::work, this));
// Something else, possibly throw
}
~worker() {
_alive = false;
_thread.reset();
}
private:
void work() {
while (_alive)
do_work();
}
};
11. Спасение от зловещего деструктора: detach vs. join
void reset()
{ _impl.detach(); }
− Небезопасно - в любой непонятной
ситуации поток будет работать с
мёртвым объектом
− Нарушение RAII
− Личная неприязнь
void reset()
{ _impl.join(); }
class worker {
atomic<bool> _alive;
raii_thread _thread;
public:
worker() : _alive(true) {
_thread = raii_thread(bind(&worker::work, this));
// Something else, possibly throw
}
~worker() {
_alive = false;
_thread.reset();
}
private:
void work() {
while (_alive)
do_work();
}
};
12. Спасение от зловещего деструктора: detach vs. join
void reset()
{ _impl.detach(); }
− Небезопасно - в любой непонятной
ситуации поток будет работать с
мёртвым объектом
− Нарушение RAII
− Личная неприязнь
void reset()
{ _impl.join(); }
+ RAII über alles
− Отсутствие у потока возможности
завершить исполняемую функцию, что
приводит к зависанию в деструкторе
class worker {
atomic<bool> _alive;
raii_thread _thread;
public:
worker() : _alive(true) {
_thread = raii_thread(bind(&worker::work, this));
// Something else, possibly throw
}
~worker() {
_alive = false;
_thread.reset();
}
private:
void work() {
while (_alive)
do_work();
}
};
13. Interrupt (pthread_cancel, boost::thread::interrupt и т.д.)
Потоку можно отправить запрос на прерывание исполнения. Тогда в целевом потоке из системных
вызовов (read, write, и т.д.) вылетит исключение специального типа. Ещё есть специальная функция,
который позволяет проверить, не был ли прерван текущий поток (pthread_testcancel, boost::thread::
interruption_point, и т.д.).
Способы прервать выполнение функции: Interrupt
14. Interrupt (pthread_cancel, boost::thread::interrupt и т.д.)
Потоку можно отправить запрос на прерывание исполнения. Тогда в целевом потоке из системных
вызовов (read, write, и т.д.) вылетит исключение специального типа. Ещё есть специальная функция,
который позволяет проверить, не был ли прерван текущий поток (pthread_testcancel, boost::thread::
interruption_point, и т.д.).
+ Прерывает ожидание на условных переменных
+ Прерывает блокирующие функции ОС (read, write, send, recv, и т.д.)
+ Практически невозможно игнорировать
Способы прервать выполнение функции: Interrupt
15. Interrupt (pthread_cancel, boost::thread::interrupt и т.д.)
Потоку можно отправить запрос на прерывание исполнения. Тогда в целевом потоке из системных
вызовов (read, write, и т.д.) вылетит исключение специального типа. Ещё есть специальная функция,
который позволяет проверить, не был ли прерван текущий поток (pthread_testcancel, boost::thread::
interruption_point, и т.д.).
+ Прерывает ожидание на условных переменных
+ Прерывает блокирующие функции ОС (read, write, send, recv, и т.д.)
+ Практически невозможно игнорировать
− Исключение из системных вызовов станет сюрпризом для многих библиотек, написанных на C.
Вероятный результат - утечка ресурсов, не разблокированные мьютексы и т.д.
− Cистемные вызовы в деструкторах могут кинуть исключение
− Сложности с портированием - на многих ОС pthread_cancel или аналогов нет (и не будет)
− C++ STL нет interrupt или аналога
− В C++14 condition_variable::wait не кидает исключений
− Необратимость - нельзя переиспользовать поток
Способы прервать выполнение функции: Interrupt
16. Способы прервать выполнение функции: булев флаг
Булев флаг в нашем примере с worker’ом - это atomic<bool> _alive
В конструкторе:
_alive = true;
В деструкторе:
_alive = false;
В прерываемой функции:
void work() {
while (_alive)
do_work();
}
17. Способы прервать выполнение функции: булев флаг
Булев флаг в нашем примере с worker’ом - это atomic<bool> _alive
В конструкторе:
_alive = true;
В деструкторе:
_alive = false;
В прерываемой функции:
void work() {
while (_alive)
do_work();
}
+ Не надо портировать
+ Для пользователя кода очевидны точки прерывания функции
18. Способы прервать выполнение функции: булев флаг
Булев флаг в нашем примере с worker’ом - это atomic<bool> _alive
В конструкторе:
_alive = true;
В деструкторе:
_alive = false;
В прерываемой функции:
void work() {
while (_alive)
do_work();
}
+ Не надо портировать
+ Для пользователя кода очевидны точки прерывания функции
− Много одинакового кода
− Этот код вне объекта потока
− Мешает декомпозиции
− Ожидание на условных переменных надо прерывать вручную
− Нельзя прервать блокирующие функции (read, write, send, recv, и т.д.)
− Проверку флага легко забыть
19. Решение - давайте сделаем простой cancellation_token
class cancellation_token {
atomic<bool> _cancelled;
public:
explicit operator bool() const
{ return !_cancelled; }
void cancel()
{ _cancelled = true; }
};
20. RAII поток? Наконец-то!
class raii_thread {
thread _impl;
cancellation_token _token;
public:
// Function must accept cancellation_token reference as first parameter
template<class Function, class... Args>
raii_thread(Function&& f, Args&&... args)
{ _impl = thread(f, ref(_token), args); }
~raii_thread() {
if (_impl.joinable())
reset();
}
void reset() {
_token.cancel();
_impl.join();
}
// Other raii_thread constructors and methods
};
21. Улучшенный worker
class worker {
raii_thread _thread;
public:
worker() {
_thread = raii_thread(bind(&worker::work, this, _1));
// Something else, possibly throw
}
~worker()
{ _thread.reset(); }
private:
void work(cancellation_token& token) {
while (token)
do_work();
}
};
Итог: минус один мембер, упрощение деструктора, безопасность в случае исключений
31. Многопоточная очередь с cancellation_token
void concurrent_queue::push(const T& t) {
unique_lock<mutex> l(_mutex);
_queue.push(t);
_condition.notify_one();
}
bool concurrent_queue::try_pop(T& t, cancellation_token& token) {
unique_lock<mutex> l(_mutex);
while (token && _queue.empty())
cancellable_wait(_condition, l, token);
if (_queue.empty())
return false;
t = _queue.front();
_queue.pop();
return true;
}
32. task_executor с cancellation_token
class task_executor {
concurrent_queue _queue;
raii_thread _thread;
public:
task_executor() { _thread = raii_thread(bind(&task_executor::work, this, _1)); }
~task_executor() { _thread.reset(); }
void add(const function<void()>& f)
{ _queue.push(f); }
private:
void work(cancellation_token& token) {
while (token) {
function<void()> f;
if (_queue.try_pop(f, token)) // No more ugly timeouts!
f();
}
}
};
33. Нужно ли прерывать mutex::lock?
С условными переменными разобрались. Что насчёт мьютекса?
− Мьютексы защищают данные, они не предназначены для ожидания
события
− У мьютекса нет нормального механизма просигналить, что lock() был
прерван
− pthread_cancel никак не влияет на мьютексы - в списке cancellable функций
его нет
Итог: мьютексы мы прерывать не будем.
43. struct pipe_interface {
size_t read(void* buf, size_t size, const cancellation_token& t = dummy_token());
size_t write(const void* buf, size_t size, const cancellation_token& t = dummy_token());
};
void thread_func(const cancellation_token& token) {
while (token) {
size_t s = _pipe.read(_buffer.data(), _buffer.size(), token);
if (s != 0)
handle_data(_buffer.data(), s);
else if (token)
handle_eof();
}
}
Проектирование интерфейсов с блокирующими функциями
44. Результаты
cancellation_token
Объект, ссылка на который явно передаётся во все длительные вызовы в данном потоке. Позволяет
узнать, был ли прерван данный поток. Можно зарегистрировать обработчик, который реализует
произвольный механизм прерывания функции.
45. cancellation_token
Объект, ссылка на который явно передаётся во все длительные вызовы в данном потоке. Позволяет
узнать, был ли прерван данный поток. Можно зарегистрировать обработчик, который реализует
произвольный механизм прерывания функции.
+ Для пользователя кода очевидны точки, в которых выполнение функции может быть
остановлено
+ Можно прервать ожидание на условных переменных
+ Можно прервать большинство блокирующих функций (read, write, send, recv, и т.д.)
+ Поддержка пользовательских механизмов прерывания функций
+ Упрощает декомпозицию объектов с длительными вызовами
+ Легко портировать - от платформы зависит только механизм прерывания системных вызовов
+ Можно прерывать отдельные задачи, а не потоки целиком
− Проверку токена можно забыть
− Накладные расходы на прерывание системных вызовов
Результаты
49. Неявная передача cancellation_token через thread-local storage
ssize_t cancellable_read(int fd, void* buffer, size_t bytes_count) {
if (cancellable_poll(fd, POLLIN, get_token_from_tls()) != POLLIN)
return 0; // read was cancelled
return read(fd, buffer, bytes_count);
}
+ Не надо передавать дополнительный аргумент
+ Легче добавить cancellation_token в существующий код
− Неочевидность
− Снижение гибкости
− Необходимо два набора методов - прерываемый и не прерываемый
− Сложности с прерыванием отдельной задачи, а не потока целиком
Отвергнутые альтернативы