Upload
sergey-platonov
View
875
Download
0
Embed Size (px)
Citation preview
RAII потоки и cancellation_token в C++
Борис Сазонов, 2016
Disclaimer
В этой презентации вы встретите:
✓ Извращения в форматировании кода✓ C-style комментарии✓ Чересчур лаконичные имена классов✓ Неэффективный код без использования move и forward✓ Отсутствие проверки ошибок✓ Невидимый “using namespace std”✓ Прочие гадости
Всё это сделано для того, чтобы сохранить разумный размер шрифта. Всегда пожалуйста.
Problem, officer?
class worker { atomic<bool> _alive; thread _thread;
public: worker() : _alive(true) { _thread = thread(bind(&worker::work, this)); // Something else... }
~worker() { _alive = false; _thread.join(); }
private: void work() { while (_alive) do_work(); }};
Problem, officer?
class worker { atomic<bool> _alive; thread _thread;
public: worker() : _alive(true) { _thread = thread(bind(&worker::work, this)); throw runtime_error("Something can throw!"); }
~worker() { _alive = false; _thread.join(); }
private: void work() { while (_alive) do_work(); }};
Bang!
class worker { atomic<bool> _alive; thread _thread;
public: worker() : _alive(true) { _thread = thread(bind(&worker::work, this)); throw runtime_error("Something can throw!"); // Bang! Calls terminate() }
~worker() { _alive = false; _thread.join(); }
private: void work() { while (_alive) do_work(); }};
Зловещий деструктор
30.3.1.3 thread destructor~thread();
If joinable() then terminate(), otherwise no effects.
Зловещий деструктор
30.3.1.3 thread destructor~thread();
If joinable() then terminate(), otherwise no effects.
Попробуем что-нибудь с этим сделать:
class raii_thread { thread _impl;
public: // raii_thread constructors and methods
~raii_thread() { if (_impl.joinable()) reset(); }
void reset() { /* ??? */ }};
void reset(){ _impl.detach(); }
Спасение от зловещего деструктора: detach vs. join
Спасение от зловещего деструктора: detach vs. join
void reset(){ _impl.detach(); }
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(); }};
Спасение от зловещего деструктора: 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(); }};
Спасение от зловещего деструктора: 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(); }};
Спасение от зловещего деструктора: 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(); }};
Interrupt (pthread_cancel, boost::thread::interrupt и т.д.)
Потоку можно отправить запрос на прерывание исполнения. Тогда в целевом потоке из системных вызовов (read, write, и т.д.) вылетит исключение специального типа. Ещё есть специальная функция, который позволяет проверить, не был ли прерван текущий поток (pthread_testcancel, boost::thread::interruption_point, и т.д.).
Способы прервать выполнение функции: Interrupt
Interrupt (pthread_cancel, boost::thread::interrupt и т.д.)
Потоку можно отправить запрос на прерывание исполнения. Тогда в целевом потоке из системных вызовов (read, write, и т.д.) вылетит исключение специального типа. Ещё есть специальная функция, который позволяет проверить, не был ли прерван текущий поток (pthread_testcancel, boost::thread::interruption_point, и т.д.).
+ Прерывает ожидание на условных переменных+ Прерывает блокирующие функции ОС (read, write, send, recv, и т.д.)+ Практически невозможно игнорировать
Способы прервать выполнение функции: Interrupt
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
Способы прервать выполнение функции: булев флаг
Булев флаг в нашем примере с worker’ом - это atomic<bool> _alive
В конструкторе: _alive = true;В деструкторе: _alive = false;В прерываемой функции:void work() { while (_alive) do_work();}
Способы прервать выполнение функции: булев флаг
Булев флаг в нашем примере с worker’ом - это atomic<bool> _alive
В конструкторе: _alive = true;В деструкторе: _alive = false;В прерываемой функции:void work() { while (_alive) do_work();}
+ Не надо портировать+ Для пользователя кода очевидны точки прерывания функции
Способы прервать выполнение функции: булев флаг
Булев флаг в нашем примере с worker’ом - это atomic<bool> _alive
В конструкторе: _alive = true;В деструкторе: _alive = false;В прерываемой функции:void work() { while (_alive) do_work();}
+ Не надо портировать+ Для пользователя кода очевидны точки прерывания функции
− Много одинакового кода− Этот код вне объекта потока− Мешает декомпозиции− Ожидание на условных переменных надо прерывать вручную− Нельзя прервать блокирующие функции (read, write, send, recv, и т.д.)− Проверку флага легко забыть
Решение - давайте сделаем простой cancellation_token
class cancellation_token { atomic<bool> _cancelled;
public: explicit operator bool() const { return !_cancelled; }
void cancel() { _cancelled = true; }};
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};
Улучшенный 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(); }};
Итог: минус один мембер, упрощение деструктора, безопасность в случае исключений
Прерывание ожидания на примитивах синхронизации
На примере многопоточной очереди
Многопоточная очередь
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, const chrono::milliseconds& timeout) { unique_lock<mutex> l(_mutex); if (_queue.empty()) _condition.wait_for(l, timeout);
if (_queue.empty()) return false;
t = _queue.front(); _queue.pop(); return true;}
Многопоточная очередь
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, const chrono::milliseconds& timeout) { unique_lock<mutex> l(_mutex); if (_queue.empty()) _condition.wait_for(l, timeout);
if (_queue.empty()) return false;
t = _queue.front(); _queue.pop(); return true;}
Памятка по условным переменным
class condition_variable { // constructors and destructor
void notify_one(); void notify_all();
void wait(unique_lock<mutex>&);
cv_status wait_for( unique_lock<mutex>&, std::chrono::duration<...>&);
// other methods};
Использование многопоточной очереди - task_executor
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) { const chrono::milliseconds timeout = 10; // TODO: Select proper timeout function<void()> f; if (_queue.try_pop(f, timeout)) f(); } }};
Использование многопоточной очереди - task_executor
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) { const chrono::milliseconds timeout = 100; // TODO: Select proper timeout function<void()> f; if (_queue.try_pop(f, timeout)) f(); } }};
Использование многопоточной очереди - task_executor
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) { const chrono::milliseconds timeout = 1000; // TODO: Select proper timeout function<void()> f; if (_queue.try_pop(f, timeout)) f(); } }};
class cancellation_token { mutex _mutex; atomic<bool> _cancelled; cancellation_handler* _handler;
public: explicit operator bool() const { return !_cancelled; }
void cancel() { unique_lock<mutex> l(_mutex); if (_handler) _handler->cancel(); _cancelled = true; }
// Register/unregister handler impl, etc.};
struct cancellation_handler { virtual void cancel() = 0;};
Продвинутый cancellation_token
Продвинутый cancellation_token
class cancellation_token { mutex _mutex; atomic<bool> _cancelled; cancellation_handler* _handler;
public: explicit operator bool() const { return !_cancelled; }
void cancel() { unique_lock<mutex> l(_mutex); if (_handler) _handler->cancel(); _cancelled = true; }
// Register/unregister handler impl, etc.};
struct cancellation_handler { virtual void cancel() = 0;};
struct cancellation_guard { using token = cancellation_token; using handler = cancellation_handler;
cancellation_guard(token& t, handler& h) { t.register_handler(h); }
~cancellation_guard() { _t.unregister_handler(); }
// Other methods and fields};
Условные переменные и cancellation_token
class cv_handler : public cancellation_handler { condition_variable& _condition; unique_lock<mutex>& _lock;
public: cv_handler(condition_variable& c, unique_lock<mutex>& l) : _condition(c), _lock(l) { }
virtual void cancel() { unique_lock l(_lock.get_mutex()); _condition.notify_all(); }};
void cancellable_wait(condition_variable& cv, unique_lock<mutex>& l, cancellation_token& t){ cv_handler handler(cv, l); // implements cancel() cancellation_guard guard(t, handler); // registers and unregisters handler cv.wait(l);}
Многопоточная очередь с 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;}
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(); } }};
Нужно ли прерывать mutex::lock?
С условными переменными разобрались. Что насчёт мьютекса?
− Мьютексы защищают данные, они не предназначены для ожидания события
− У мьютекса нет нормального механизма просигналить, что lock() был прерван
− pthread_cancel никак не влияет на мьютексы - в списке cancellable функций его нет
Итог: мьютексы мы прерывать не будем.
Прерывание блокирующих функций ОС*Для случая, где ОС = POSIX
Бестиарий блокирующих функций POSIX
Файловые дескрипторыssize_t read(int file_descriptor, void* buffer, size_t bytes_count);ssize_t write(int file_descriptor, const void* buffer, size_t bytes_count);
Сокетыssize_t recv(int socket, void *buffer, size_t length, int flags);ssize_t send(int socket, const void *buffer, size_t length, int flags);ssize_t recvmsg(int socket, struct msghdr *message, int flags);ssize_t sendmsg(int socket, const struct msghdr *message, int flags);int accept(int socket, struct sockaddr *restrict address, socklen_t *address_len);int connect(int socket, const struct sockaddr *address, socklen_t address_len);
Бестиарий блокирующих функций POSIX
Файловые дескрипторыssize_t read(int file_descriptor, void* buffer, size_t bytes_count); // POLLINssize_t write(int file_descriptor, const void* buffer, size_t bytes_count); // POLLOUT
Сокетыssize_t recv(int socket, void *buffer, size_t length, int flags); // POLLINssize_t send(int socket, const void *buffer, size_t length, int flags); // POLLOUTssize_t recvmsg(int socket, struct msghdr *message, int flags); // POLLINssize_t sendmsg(int socket, const struct msghdr *message, int flags); // POLLOUTint accept(int socket, struct sockaddr *restrict address, socklen_t *address_len); // POLLINint connect(int socket, const struct sockaddr *address, socklen_t address_len); // POLLOUT
Решение: будем ждать появления данных (места в буфере, подключения, и т.д.) не в этих вызовах, а в функции poll, которую и будем прерывать.int poll(struct pollfd fds[], nfds_t nfds, int timeout);
Прерываем poll: poll_cancellation_handler
class poll_cancellation_handler : public cancellation_handler { int _pipe[2];
public: poll_cancellation_handler() { pipe(_pipe); }
~poll_cancellation_handler() { close(_pipe[0]); close(_pipe[1]); }
virtual void cancel() { char dummy = 0; write(_pipe[1], &dummy, 1); }
int get_fd() const { return _pipe[0]; }};
Прерываем всё: cancellable_poll и cancellable_read
short cancellable_poll(int fd, short events, cancellation_token& token) { poll_cancellation_handler handler; cancellation_guard guard(token, handler);
pollfd polled_fd = { .fd = fd, .events = events }; pollfd cancel_fd = { .fd = handler.get_fd(), .events = POLLIN }; pollfd fds[2] = { polled_fd, cancel_fd }; poll(fds, 2, -1);
return fds[0].revents;}
Прерываем всё: cancellable_poll и cancellable_read
short cancellable_poll(int fd, short events, cancellation_token& token) { poll_cancellation_handler handler; cancellation_guard guard(token, handler);
pollfd polled_fd = { .fd = fd, .events = events }; pollfd cancel_fd = { .fd = handler.get_fd(), .events = POLLIN }; pollfd fds[2] = { polled_fd, cancel_fd }; poll(fds, 2, -1);
return fds[0].revents;}
ssize_t cancellable_read(int fd, void* buffer, size_t bytes_count, cancellation_token& token) { if (cancellable_poll(fd, POLLIN, token) != POLLIN) return 0; // read was cancelled return read(fd, buffer, bytes_count);}
struct pipe_interface { size_t read(void* buf, size_t size); size_t write(const void* buf, size_t size);};
Проектирование интерфейсов с блокирующими функциями
struct pipe_interface { size_t read(void* buf, size_t size); size_t write(const void* buf, size_t size);
size_t read(void* buf, size_t size, cancellation_token& t); size_t write(const void* buf, size_t size, cancellation_token& t);};
Проектирование интерфейсов с блокирующими функциями
struct pipe_interface { //size_t read(void* buf, size_t size); //size_t write(const void* buf, size_t size);
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());};
// cancellation_token.hppstruct dummy_token : public cancellation_token { virtual bool is_cancelled() const { return false; }
virtual void register(cancellation_handler&) const { }
// Unregister handler, etc.};
Проектирование интерфейсов с блокирующими функциями
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(); }}
Проектирование интерфейсов с блокирующими функциями
Результаты
cancellation_token
Объект, ссылка на который явно передаётся во все длительные вызовы в данном потоке. Позволяет узнать, был ли прерван данный поток. Можно зарегистрировать обработчик, который реализует произвольный механизм прерывания функции.
cancellation_token
Объект, ссылка на который явно передаётся во все длительные вызовы в данном потоке. Позволяет узнать, был ли прерван данный поток. Можно зарегистрировать обработчик, который реализует произвольный механизм прерывания функции.
+ Для пользователя кода очевидны точки, в которых выполнение функции может быть остановлено
+ Можно прервать ожидание на условных переменных+ Можно прервать большинство блокирующих функций (read, write, send, recv, и т.д.)+ Поддержка пользовательских механизмов прерывания функций+ Упрощает декомпозицию объектов с длительными вызовами+ Легко портировать - от платформы зависит только механизм прерывания системных вызовов+ Можно прерывать отдельные задачи, а не потоки целиком
− Проверку токена можно забыть− Накладные расходы на прерывание системных вызовов
Результаты
Отвергнутые альтернативы
cancelled_exception
ssize_t cancellable_read(int fd, void* buffer, size_t bytes_count, cancellation_token& token) {
if (cancellable_poll(fd, POLLIN, token) != POLLIN)
throw cancelled_exception("Read was cancelled!");
return read(fd, buffer, bytes_count);
}
cancelled_exception
ssize_t cancellable_read(int fd, void* buffer, size_t bytes_count, cancellation_token& token) {
if (cancellable_poll(fd, POLLIN, token) != POLLIN)
throw cancelled_exception("Read was cancelled!");
return read(fd, buffer, bytes_count);
}
+ Сложнее проигнорировать+ Нет трудностей с возвращаемыми значениями− Снижение гибкости− Нарушение философии исключений− Усложнение отладки
Отвергнутые альтернативы
Неявная передача 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 через 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 в существующий код− Неочевидность− Снижение гибкости− Необходимо два набора методов - прерываемый и не прерываемый− Сложности с прерыванием отдельной задачи, а не потока целиком
Отвергнутые альтернативы
https://github.com/bo-on-software/rethread
Вопросы?
Спасибо за внимание!