52
RAII потоки и cancellation_token в C++ Борис Сазонов, 2016

Борис Сазонов, RAII потоки и CancellationToken в C++

Embed Size (px)

Citation preview

Page 1: Борис Сазонов, RAII потоки и CancellationToken в C++

RAII потоки и cancellation_token в C++

Борис Сазонов, 2016

Page 2: Борис Сазонов, RAII потоки и CancellationToken в C++

Disclaimer

В этой презентации вы встретите:

✓ Извращения в форматировании кода✓ C-style комментарии✓ Чересчур лаконичные имена классов✓ Неэффективный код без использования move и forward✓ Отсутствие проверки ошибок✓ Невидимый “using namespace std”✓ Прочие гадости

Всё это сделано для того, чтобы сохранить разумный размер шрифта. Всегда пожалуйста.

Page 3: Борис Сазонов, RAII потоки и CancellationToken в C++

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(); }};

Page 4: Борис Сазонов, RAII потоки и CancellationToken в C++

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(); }};

Page 5: Борис Сазонов, RAII потоки и CancellationToken в C++

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(); }};

Page 6: Борис Сазонов, RAII потоки и CancellationToken в C++

Зловещий деструктор

30.3.1.3 thread destructor~thread();

If joinable() then terminate(), otherwise no effects.

Page 7: Борис Сазонов, RAII потоки и CancellationToken в C++

Зловещий деструктор

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() { /* ??? */ }};

Page 8: Борис Сазонов, RAII потоки и CancellationToken в C++

void reset(){ _impl.detach(); }

Спасение от зловещего деструктора: detach vs. join

Page 9: Борис Сазонов, RAII потоки и CancellationToken в C++

Спасение от зловещего деструктора: 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(); }};

Page 10: Борис Сазонов, RAII потоки и CancellationToken в C++

Спасение от зловещего деструктора: 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(); }};

Page 11: Борис Сазонов, RAII потоки и CancellationToken в C++

Спасение от зловещего деструктора: 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(); }};

Page 12: Борис Сазонов, RAII потоки и CancellationToken в C++

Спасение от зловещего деструктора: 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(); }};

Page 13: Борис Сазонов, RAII потоки и CancellationToken в C++

Interrupt (pthread_cancel, boost::thread::interrupt и т.д.)

Потоку можно отправить запрос на прерывание исполнения. Тогда в целевом потоке из системных вызовов (read, write, и т.д.) вылетит исключение специального типа. Ещё есть специальная функция, который позволяет проверить, не был ли прерван текущий поток (pthread_testcancel, boost::thread::interruption_point, и т.д.).

Способы прервать выполнение функции: Interrupt

Page 14: Борис Сазонов, RAII потоки и CancellationToken в C++

Interrupt (pthread_cancel, boost::thread::interrupt и т.д.)

Потоку можно отправить запрос на прерывание исполнения. Тогда в целевом потоке из системных вызовов (read, write, и т.д.) вылетит исключение специального типа. Ещё есть специальная функция, который позволяет проверить, не был ли прерван текущий поток (pthread_testcancel, boost::thread::interruption_point, и т.д.).

+ Прерывает ожидание на условных переменных+ Прерывает блокирующие функции ОС (read, write, send, recv, и т.д.)+ Практически невозможно игнорировать

Способы прервать выполнение функции: Interrupt

Page 15: Борис Сазонов, RAII потоки и CancellationToken в C++

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

Page 16: Борис Сазонов, RAII потоки и CancellationToken в C++

Способы прервать выполнение функции: булев флаг

Булев флаг в нашем примере с worker’ом - это atomic<bool> _alive

В конструкторе: _alive = true;В деструкторе: _alive = false;В прерываемой функции:void work() { while (_alive) do_work();}

Page 17: Борис Сазонов, RAII потоки и CancellationToken в C++

Способы прервать выполнение функции: булев флаг

Булев флаг в нашем примере с worker’ом - это atomic<bool> _alive

В конструкторе: _alive = true;В деструкторе: _alive = false;В прерываемой функции:void work() { while (_alive) do_work();}

+ Не надо портировать+ Для пользователя кода очевидны точки прерывания функции

Page 18: Борис Сазонов, RAII потоки и CancellationToken в C++

Способы прервать выполнение функции: булев флаг

Булев флаг в нашем примере с worker’ом - это atomic<bool> _alive

В конструкторе: _alive = true;В деструкторе: _alive = false;В прерываемой функции:void work() { while (_alive) do_work();}

+ Не надо портировать+ Для пользователя кода очевидны точки прерывания функции

− Много одинакового кода− Этот код вне объекта потока− Мешает декомпозиции− Ожидание на условных переменных надо прерывать вручную− Нельзя прервать блокирующие функции (read, write, send, recv, и т.д.)− Проверку флага легко забыть

Page 19: Борис Сазонов, RAII потоки и CancellationToken в C++

Решение - давайте сделаем простой cancellation_token

class cancellation_token { atomic<bool> _cancelled;

public: explicit operator bool() const { return !_cancelled; }

void cancel() { _cancelled = true; }};

Page 20: Борис Сазонов, RAII потоки и CancellationToken в C++

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};

Page 21: Борис Сазонов, RAII потоки и CancellationToken в C++

Улучшенный 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(); }};

Итог: минус один мембер, упрощение деструктора, безопасность в случае исключений

Page 22: Борис Сазонов, RAII потоки и CancellationToken в C++

Прерывание ожидания на примитивах синхронизации

На примере многопоточной очереди

Page 23: Борис Сазонов, RAII потоки и CancellationToken в C++

Многопоточная очередь

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;}

Page 24: Борис Сазонов, RAII потоки и CancellationToken в C++

Многопоточная очередь

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};

Page 25: Борис Сазонов, RAII потоки и CancellationToken в C++

Использование многопоточной очереди - 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(); } }};

Page 26: Борис Сазонов, RAII потоки и CancellationToken в C++

Использование многопоточной очереди - 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(); } }};

Page 27: Борис Сазонов, RAII потоки и CancellationToken в C++

Использование многопоточной очереди - 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(); } }};

Page 28: Борис Сазонов, RAII потоки и CancellationToken в C++

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

Page 29: Борис Сазонов, RAII потоки и CancellationToken в C++

Продвинутый 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};

Page 30: Борис Сазонов, RAII потоки и CancellationToken в C++

Условные переменные и 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);}

Page 31: Борис Сазонов, RAII потоки и CancellationToken в C++

Многопоточная очередь с 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;}

Page 32: Борис Сазонов, RAII потоки и CancellationToken в C++

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(); } }};

Page 33: Борис Сазонов, RAII потоки и CancellationToken в C++

Нужно ли прерывать mutex::lock?

С условными переменными разобрались. Что насчёт мьютекса?

− Мьютексы защищают данные, они не предназначены для ожидания события

− У мьютекса нет нормального механизма просигналить, что lock() был прерван

− pthread_cancel никак не влияет на мьютексы - в списке cancellable функций его нет

Итог: мьютексы мы прерывать не будем.

Page 34: Борис Сазонов, RAII потоки и CancellationToken в C++

Прерывание блокирующих функций ОС*Для случая, где ОС = POSIX

Page 35: Борис Сазонов, RAII потоки и CancellationToken в C++

Бестиарий блокирующих функций 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);

Page 36: Борис Сазонов, RAII потоки и CancellationToken в C++

Бестиарий блокирующих функций 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);

Page 37: Борис Сазонов, RAII потоки и CancellationToken в C++

Прерываем 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]; }};

Page 38: Борис Сазонов, RAII потоки и CancellationToken в C++

Прерываем всё: 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;}

Page 39: Борис Сазонов, RAII потоки и CancellationToken в C++

Прерываем всё: 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);}

Page 40: Борис Сазонов, RAII потоки и CancellationToken в C++

struct pipe_interface { size_t read(void* buf, size_t size); size_t write(const void* buf, size_t size);};

Проектирование интерфейсов с блокирующими функциями

Page 41: Борис Сазонов, RAII потоки и CancellationToken в C++

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);};

Проектирование интерфейсов с блокирующими функциями

Page 42: Борис Сазонов, RAII потоки и CancellationToken в C++

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.};

Проектирование интерфейсов с блокирующими функциями

Page 43: Борис Сазонов, RAII потоки и CancellationToken в C++

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(); }}

Проектирование интерфейсов с блокирующими функциями

Page 44: Борис Сазонов, RAII потоки и CancellationToken в C++

Результаты

cancellation_token

Объект, ссылка на который явно передаётся во все длительные вызовы в данном потоке. Позволяет узнать, был ли прерван данный поток. Можно зарегистрировать обработчик, который реализует произвольный механизм прерывания функции.

Page 45: Борис Сазонов, RAII потоки и CancellationToken в C++

cancellation_token

Объект, ссылка на который явно передаётся во все длительные вызовы в данном потоке. Позволяет узнать, был ли прерван данный поток. Можно зарегистрировать обработчик, который реализует произвольный механизм прерывания функции.

+ Для пользователя кода очевидны точки, в которых выполнение функции может быть остановлено

+ Можно прервать ожидание на условных переменных+ Можно прервать большинство блокирующих функций (read, write, send, recv, и т.д.)+ Поддержка пользовательских механизмов прерывания функций+ Упрощает декомпозицию объектов с длительными вызовами+ Легко портировать - от платформы зависит только механизм прерывания системных вызовов+ Можно прерывать отдельные задачи, а не потоки целиком

− Проверку токена можно забыть− Накладные расходы на прерывание системных вызовов

Результаты

Page 46: Борис Сазонов, RAII потоки и CancellationToken в C++

Отвергнутые альтернативы

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);

}

Page 47: Борис Сазонов, RAII потоки и CancellationToken в C++

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);

}

+ Сложнее проигнорировать+ Нет трудностей с возвращаемыми значениями− Снижение гибкости− Нарушение философии исключений− Усложнение отладки

Отвергнутые альтернативы

Page 48: Борис Сазонов, RAII потоки и CancellationToken в C++

Неявная передача 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);

}

Отвергнутые альтернативы

Page 49: Борис Сазонов, RAII потоки и CancellationToken в C++

Неявная передача 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 в существующий код− Неочевидность− Снижение гибкости− Необходимо два набора методов - прерываемый и не прерываемый− Сложности с прерыванием отдельной задачи, а не потока целиком

Отвергнутые альтернативы

Page 50: Борис Сазонов, RAII потоки и CancellationToken в C++

https://github.com/bo-on-software/rethread

[email protected]

Page 51: Борис Сазонов, RAII потоки и CancellationToken в C++

Вопросы?

Page 52: Борис Сазонов, RAII потоки и CancellationToken в C++

Спасибо за внимание!