ПВТ - весна 2015 - Лекция 5. Многопоточное...

176
Лекция 5. Многопоточное программирование в языке С++. Работа с потоками. Защита данных. Синхронизация. Будущие результаты Пазников Алексей Александрович Кафедра вычислительных систем СибГУТИ Сайт курса: http://cpct.sibsutis.ru/~apaznikov/teaching/ Q/A: https://piazza.com/sibsutis.ru/spring2015/pct2015spring Параллельные вычислительные технологии Весна 2015 (Parallel Computing Technologies, PCT 15)

Upload: alexey-paznikov

Post on 15-Jul-2015

369 views

Category:

Education


1 download

TRANSCRIPT

Лекция 5. Многопоточное программирование в языке С++. Работа с потоками. Защита данных. Синхронизация. Будущие результаты

Пазников Алексей АлександровичКафедра вычислительных систем СибГУТИ

Сайт курса: http://cpct.sibsutis.ru/~apaznikov/teaching/Q/A: https://piazza.com/sibsutis.ru/spring2015/pct2015spring

Параллельные вычислительные технологииВесна 2015 (Parallel Computing Technologies, PCT 15)

Создание и завершение работы потоков

2

О дивный новый [параллельный] мир!

#include <iostream>#include <thread>

void hello() { // функция, которая реализует поток std::cout << "hello brave new world!\n";}

int main() { std::thread t(hello); // создаём поток t.join(); // дожидаемся завершения}

3

Запуск потока

class thread_class { // класс с перегруженным оператором()public: void operator()() const { hello(); bye(); }}

#include <iostream>#include <thread>

void hello() { std::cout << "hello brave new world!\n";}

int main() { std::thread t(hello); t.join();}

4

Запуск потока

class thread_class {public: void operator()() const { hello(); bye(); }}

std::thread thr((thread_class())); std::thread thr{thread_class} // так лучше!

std::thread thr([](){ std::cout << "hello world\n";});thr.join();

“Most vexing parse”

5

Отсоединённый поток

struct func { int &i;

func(int &_i): i{_i} {}

void operator()() { std::cout << i << std::endl;// доступ к висячей сслыке }};

int main(int argc, const char *argv[]){ int local = 100; func myfunc(local); std::thread thr(myfunc); thr.detach(); // отсоединяем поток...} // поток ещё работает!

6

Ожидание завершения потока в случае исключения

std::thread thr(myfunc);

try { std::cout << "hello"; // ... throw "error"; }

catch (...) { thr.join(); // не забыть дождаться завершения std::cout << "exception catched\n"; return 1; }

thr.join();

7

RAII - передача ресурса есть инициализация

class thread_guard { std::thread &t;public: explicit thread_guard(std::thread &_t): t{_t} {} ~thread_guard() { if (t.joinable()) { t.join(); } } thread_guard(thread_guard const&) = delete; thread_guard &operator=(thread_guard const&) = delete;};

void foo() { int local; std::thread t{func(local)}; thread_guard g(t); do_some_work();} // t.join()

8

Запуск нескольких потоков и ожидание завершения

int main() { std::vector<std::thread> threads;

for (auto i = 0; i < 10; i++) { threads.push_back(std::thread([i](){ std::cout << i << "\n"; })); }

for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join));}

9

Запуск нескольких потоков и идентификаторы потоков

std::vector<std::thread> threads;std::map<std::thread::id, int> table;

for (auto i = 0; i < 10; i++) { threads.push_back(std::thread([i](){ std::this_thread::sleep_for( std::chrono::milliseconds(100 * i)); std::cout << i << "\n"; })); table.insert(std::make_pair(threads.back().get_id(), i % 2));}

std::cout << "value of 5: " << table[threads[5].get_id()] << std::endl;std::cout << "value of 6: " << table[threads[6].get_id()] << std::endl;

for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join)); 10

Передача аргументов функции потока

void func(int i, std::string const &s1, std::string const &s2) { std::cout << s1 << " " << s2 << std::endl;}

int main() { std::thread t(func, 2014, "hello", "world"); t.join();}

11

Передача аргументов функции потока

void func(int i, std::string const &s1, std::string const &s2) { std::cout << s1 << " " << s2 << std::endl;}

int main() { char buf[] = "hello"; std::thread t(func, 2014, buf, "world"); t.detach();}

12

Передача аргументов функции потока

void func(int i, std::string const &s1, std::string const &s2) { std::cout << s1 << " " << s2 << std::endl;}

int main() { char buf[] = "hello"; // автоматическая переменная std::thread t(func, 2014, buf, "world"); t.detach();} // переменной foo нет, // а поток продолжает выполняться

Использование висячего указателя

13

Передача аргументов функции потока

void func(int i, std::string const &s1, std::string const &s2) { std::cout << s1 << " " << s2 << std::endl;}

int main() { char buf[] = "hello"; std::thread t(func, 2014, std::string(buf), "world"); t.detach();}

Явное преобразование позволяет избежать висячего указателя

14

Передача аргументов функции потока

void func(std::string &s_arg) { s_arg = "hello parallel world";}

int main() { std::string s{"hello world"}; std::thread t(func, s); t.join(); std::cout << s << std::endl; // результат операции? return 0;}

15

Передача аргументов функции потока по ссылке

void func(std::string &s_arg) { s_arg = "hello parallel world";}

int main() { std::string s{"hello world"}; std::thread t(func, std::ref(s)); // передача по ссылке! t.join(); std::cout << s << std::endl; // hello parallel world return 0;}

16

Передача управления потоком

struct bulky { // некий массивный объект std::string name; void print() { std::cout << "I'm " << name << std::endl; }};

void func(std::unique_ptr<bulky> obj) { obj->print();}

int main() { std::unique_ptr<bulky> ptr{new bulky{"Ivan"}}; std::thread t(func, ptr); t.join();}

17

Передача аргументов

struct bulky { // некий массивный объект std::string name; void print() { std::cout << "I'm " << name << std::endl; }};

void func(std::unique_ptr<bulky> obj) { obj->print();}

int main() { std::unique_ptr<bulky> ptr{new bulky{"Ivan"}}; std::thread t(func, std::move(ptr)); t.join();}

18

Передача управления потоком

void foo() { }void bar() { }

int main() { std::thread t1(foo); std::thread t2 = std::move(t1); // перемещение t1 = std::thread(bar); std::thread t3; t1 = std::move(t3); // ошибка! t3 = std::move(t2); t3 = std::move(t2); // ошибка!

t1.join(); t2.join(); // ошибка! t3.join();}

19

Передача управления потоком

std::thread foo() { std::thread thr([](){ std::cout << "thread\n"; }); return thr; // перемещение}

void bar(std::thread thr) { thr.join(); }

int main() { std::thread thr = foo(); bar(std::move(thr)); // перемещение

return 0;}

20

RAII - передача ресурса есть инициализация

class scoped_thread { std::thread &t;public: explicit scoped_thread(std::thread &_t): t{std::move(_t)} { if (!t.joinable()) throw std::logic_error("no thread"); }

~scoped_thread() { t.join(); } scoped_thread(thread_guard const&) = delete; scoped_thread &operator=(thread_guard const&) = delete;};

void foo() { int local; scoped_thread t{std::thread(func((local))};} // ~scoped_thread()

21

RAII - передача ресурса есть инициализация

try { scoped_thread t{std::thread(func((local))}; // ... if (cond) throw "error"; // ~scoped_thread() }

catch (...) { std::cout << "exception catched\n"; return 1; }

22

Параллельная версия алгоритма accumulate

Поток 1 Поток 2 Поток num_threads

CPU CPU CPU

accumulate_block accumulate_block accumulate_block

23

Параллельная версия алгоритма accumulate

const int SIZE = 10;

template<typename Iterator, typename T, typename BinOperation>struct accumulate_block { // каждый поток рассчитывает свой блок void operator()(Iterator first, Iterator last, T& result, BinOperation op) { result = std::accumulate(first, last, result, op); }};

24

Параллельная версия алгоритма accumulate

template<typename Iterator, typename T, typename BinOp>T parallel_accumulate(Iterator first, Iterator last, T init, BinOp op) { unsigned long const length = std::distance(first, last);

if (!length) return init;

unsigned long const min_per_thread = 2;

unsigned long const max_threads = (length + min_per_thread - 1) / min_per_thread;

unsigned long const hardware_threads = std::thread::hardware_concurrency();

unsigned long const num_threads = std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);

unsigned long const block_size = length / num_threads;

std::vector<T> results(num_threads); std::vector<std::thread> threads(num_threads - 1);

аппаратный предел числа потоков (ядер)

макс. число потоков

число потоков размер блока

25

Параллельная версия алгоритма accumulate

Iterator block_start = first;

for (unsigned long i = 0; i < num_threads - 1; i++) { Iterator block_end = block_start;

std::advance(block_end, block_size); // конец блока

// каждый поток рассчитывает свой блок threads[i] = std::thread( accumulate_block<Iterator, T, BinOperation>(), block_start, block_end, std::ref(results[i]), op);

block_start = block_end; }

accumulate_block<Iterator, T, BinOperation>() (block_start, last, results[num_threads - 1], op);

std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join));

return std::accumulate(results.begin(), results.end(), init, op); }

последний блок (+остаток)

26

Параллельная версия алгоритма accumulate

int main() { std::vector<int> vec(SIZE);

for (auto &x: vec) x = rand() % 10;

std::cout << "acc: " << parallel_accumulate(vec.begin(), vec.end(), 0, std::plus<int>()) << std::endl;

return 0;}

27

Защита разделяемых данных между потоками и синхронизация

28

Мьютексы в С++

std::list<int> mylist;std::mutex lock;

void add(int elem) { std::lock_guard<std::mutex> guard(lock); mylist.push_back(elem);}

bool find(int elem) { std::lock_guard<std::mutex> guard(lock); return std::find(mylist.begin(), mylist.end(), elem) != mylist.end();}

29

Мьютексы в С++

std::list<int> mylist;std::mutex lock;

void add(int elem) { std::lock_guard<std::mutex> guard(lock); if (elem < 0) throw "error"; mylist.push_back(elem);}

bool find(int elem) { std::lock_guard<std::mutex> guard(lock); if (mylist.size() == 0) return false; return std::find(mylist.begin(), mylist.end(), elem) != mylist.end();}

30

Мьютексы в С++

class wrapper {private: data_t data; // защищаемые данные std::mutex mut;public: template<typename Function> void proc_data(Function func) { std::lock_guard<std::mutex> lock(mut); func(data); }};

data_t *unprotected; // внешний указатель

void unsafe_func(data_t &protected) { unprotected = &protected; }

wrapper obj;obj.proc_data(unsafe_func); unprotected->do_something(); // незащищённый доступ к data

Любой код, имеющий доступ к указателю или ссылке, может делать с ним всё, что угодно, не захватывая мьютекс.

31

Мьютексы в С++

class wrapper {private: data_t data; // защищаемые данные std::mutex mut;public: template<typename Function> void proc_data(Function func) { std::lock_guard<std::mutex> lock(mut); func(data); }};

data_t *unprotected; // внешний указатель

void unsafe_func(data_t &protected) { unprotected = &protected; }

wrapper obj;obj.proc_data(unsafe_func); unprotected->do_something(); // незащищённый доступ к data

Любой код, имеющий доступ к указателю или ссылке, может делать с ним всё, что угодно, не захватывая мьютекс.

Нельзя передавать указатели и ссылки на защищённые данные за пределы области видимости блокировки никаким образом.

32

Адаптация интерфейсов к многопоточности

template <...> class stack {public: // ...

bool empty() const;

size_t size() const;

T& top();

T const &top() const;

void push(T const&);

void push(T&&);

void pop();

void swap(stack&&);

};

33

Адаптация интерфейсов к многопоточности

template <...> class stack {public: // ...

bool empty() const;

size_t size() const;

T& top();

T const &top() const;

void push(T const&);

void push(T&&);

void pop();

void swap(stack&&);

};

34

Адаптация интерфейсов к многопоточности

template <...> class stack {public: // ...

bool empty() const;

size_t size() const;

T& top();

T const &top() const;

void push(T const&);

void push(T&&);

void pop();

void swap(stack&&);

};

некорректный результат как решить?

stack<int> s;

if (!s.empty()) {

int const value = s.top();

s.pop();

// ...

}

35

Адаптация интерфейсов к многопоточности

std::vector<int> result;mystack.pop(result);

1. Передавать ссылку в функцию pop

2. Потребовать наличия копирующего или перемещающего конструктора, не возбуждающего исключений (доказано, что можно объединить pop и top, но это можно сделать только если конструкторы не вызывают исключений)

3. Возвращать указатель на вытолкнутый элемент

4. Одновременно 1 и один из вариантов 2 или 3

std::shared_ptr<T> pop()

36

Потокобезопасный стек

template<typename T>class safe_stack {private: std::stack<T> data; mutable std::mutex m;public: safe_stack(); safe_stack(const safe_stack&);

// стек нельзя присваивать safe_stack& operator=(const safe_stack&) = delete; void push(T new_value); std::shared_ptr<T> pop(); void pop(T& value); bool empty() const;

// swap отсутствует // -- интерфейс предельно упрощён --};

37

Потокобезопасный стек

safe_stack(const safe_stack &rhs) { std::lock_guard<std::mutex> lock(rhs.m); data = rhs.data;}

std::shared_ptr<T> pop() { std::lock_guard<std::mutex> lock(m); if (data.empty()) throw empty_stack(); // выделяем память под возвращаемое значение std::shared_ptr<T> const res(std::make_shared<T>(data.top())); data.pop(); return res;}

void pop(T& value) { std::lock_guard<std::mutex> lock(m); if (data.empty()) throw empty_stack(); value = data.top(); data.pop(); }

38

Дедлоки: захват нескольких мьютексов

class Widget {private: data obj; std::mutex m;public: Widget(data const &d): data(d) {} friend void swap(Widget &lhs, Widget &rhs) { if (&lhs == &rhs) return; std::lock(lhs.m, rhs.m); // захыватываем мьютекс

// adopt_lock: lock_a и lock_b начинают владеть // захваченной блокировкой std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock); std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock); swap(lhs.some_detail, rhs.some_detail); }};

lock - “всё или ничего”

39

Дедлоки: иерархические мьютексы

hierarchical_mutex(100) hierarchical_mutex(50) hierarchical_mutex(30)

1 -> 2 -> 3

1

2

3

2 -> 3 3 -> 1 3 -> 2

порядок запирания 40

Дедлоки: иерархические мьютексы

hierarchical_mutex(100) hierarchical_mutex(50) hierarchical_mutex(30)

1

2

3

порядок запирания

1 -> 2 -> 3 2 -> 3 3 -> 1 3 -> 2

41

Дедлоки: иерархические мьютексы - пример

hier_mutex high_level_mut(10000);hier_mutex low_level_mut(5000);

int low_level_func() { // низкоуровневая блокировка std::lock_guard<hier_mutex> lock(low_level_mut); do_low_level_stuff();}

void high_level_func() { // высокоуровневая блокировка std::lock_guard<hier_mut> lock(high_level_mut); do_high_level_stuff(low_level_func()); // корректный } // порядок

void thread_a() { high_level_func(); // всё ок}

hier_mutex lowest_level_mut(100);void thread_b() { // некорректный порядок блокировки! std::lock_guard<hier_mut> lock(lowest_level_mut); high_level_func(); // вызов недопустим} 42

Иерархические мьютексы - возможная реализация

class hier_mutex { // hierarchical mutexprivate:

std::mutex internal_mut; unsigned long const hier_val; // текущий уровень unsigned prev_hier_val; // предыдущий уровень

// уровень иерархии текущего потока static thread_local unsigned long this_thread_hier_val;

void check_for_hier_violation() { if (this_thread_hier_val <= hier_val) { throw std::logic_error("mutex hierarchy violated"); } }

// обновить текущий уровень иерархии потока void update_hier_val() { prev_hier_val = this_thread_hier_val; this_thread_hier_val = hier_val; }

43

Иерархические мьютексы - возможная реализация

public: explicit hier_mutex(unsigned long value): hier_val(value), prev_hier_value(0) {}

void lock() { check_for_hier_violation(); internal_mutex.lock(); update_hier_val(); }

void unlock() { this_thrad_hier_val = prev_hier_val; internal_mutex.unlock(); }

void trylock() { // ... }

thread_local unsigned long hier_mutex::this_thread_hier_val(ULONG_MAX);

44

Блокировка с помощью std::unique_lock

class Widget {

int val;

std::mutex m;

int getval() const {

return val; }

};

bool Cmp(Widget &lhs, Widget &rhs) { // не захватываем пока мьютексы std::unique_lock<std::mutex> lock1(lhs.m,std::defer_lock); std::unique_lock<std::mutex> lock2(rhs.m,std::defer_lock);

// а вот сейчас захватываем, причём без дедлоков std::lock(lock1, lock2); return lhs.getval() > rhs.getval() ? true : false;}

45

Блокировка с помощью std::unique_lock

class Widget {

int val;

std::mutex m;

int getval() const {

std::lock_guard<std::mutex> lock(m);

return val; }

};

bool Cmp(Widget &lhs, Widget &rhs) { // обе операции совершаются под защитой мьютекса int const lhs_val = lhs.getval(); int const rhs_val = rhs.getval(); std::lock(lock1, lock2);

return lhs_val > rhs_val ? true : false;}

Минимизация гранулярности блокировки!

46

Блокировка с помощью std::unique_lock

void pop_and_process() { std::unique_lock<std::mutex> lock(mut); Widget data = queue.pop(); // получить элемент данных lock.unlock(); // освободить мьютекс super_widget result = process(data); // обработать данные lock.lock(); // опять захватить мьютекс output_result(data, result); // вывести результат}

Минимизация блокировок!

▪ блокировать данные, а не операции▪ удерживать мьютекс столько, сколько необходимо

▫ тяжёлые операции (захват другого мьютекса, ввод/вывод и т.д.) - вне текущей критической секции

47

Однократный вызов и отложенная инициализация

class NetFacility {private: connect_handle connection; bool connection_flag; void open_connection() { connection = connect_manager.open(); }

public: NetFacility(connect_info &_info): {} void send_data(data_packet const &d) { // отложенная инициализация if (connection_flag == false) connection = open_connection(); connection.send(data); }

void recv_data() { /* ... */ }} А если несколько

потоков?48

Однократный вызов и отложенная инициализация

class NetFacility {private: connect_handle connection; bool connection_flag; std::mutex mut; void open_connection() { connection = connect_manager.open(); }

public: NetFacility(connect_info &_info): {} void send_data(data_packet const &d) { std::unique_lock<std::mutex> lock(mut); if (connection_flag == false) // только инициализация требует защиты! connection = open_connection(); mut.unlock(); connection.send(data); }

void recv_data() { /* ... */ } };

Защищать только инициализацию

49

Однократный вызов и отложенная инициализация

class NetFacility {private: connect_handle connection; bool connection_flag; std::mutex mut; void open_connection() { connection = connect_manager.open(); }

public: NetFacility(connect_info &_info): {} void send_data(data_packet const &d) { if (connection_flag == false) { // гонка! std::lock_guard<std::mutex> lock(mut); if (connection_flag == false) { connection = open_connection(); connection_flag = true; // гонка! } }

connection.send(data); }

двойная проверка

50

Однократный вызов и отложенная инициализация

class NetFacility {private: connect_handle connection; std::once_flag connection_flag; void open_connection() { connection = connect_manager.open(info); }

public: NetFacility(connect_info &_info): {} void send_data(data_packet const &d) { // вызывается только один раз std::call_once(connection_flag, &NetFacility::open_connection, this); connection.send(data); }

void recv_data() { /* ... */ }}

51

R/W-мьютексы в С++

class Widget { mutable std::shared_timed_mutex mut; int data;public: Widget& operator=(const R& rhs) { // эксклюзивные права на запись в *this std::unique_lock<std::shared_timed_mutex> lhs(mut, std::defer_lock);

// разделяемые права на чтение rhs std::shared_lock<std::shared_timed_mutex> rhs(other.mut, std::defer_lock);

std::lock(lhs, rhs);

// выполнить присваивание data = rhs.data; return *this; }};

52

R/W-мьютексы в С++

int Widget::read() { std::shared_lock<shared_timed_mutex> lock(mut); return val;}

void Widget::set_value(int _val) { std::lock_guard<shared_mutex> lock(mut); val = _val;}

53

Рекурсивные мьютексы

▪ std::recursive_mutex

▪ мьютекс можно запирать несколько раз в одном потоке

▪ освобождать мьютекс требуется столько раз, сколько он был захвачен

▪ использование - аналогично std::mutex (std::lock_guard, std::unique_lock, …)

54

Условные переменные

▪ std::condition_variable, std::condition_variable_any

условная переменная, необходимо взаимодействие с мьютексом (condition_variable) или с любым классом (condition_variable_any), подобным мьютексу

▪ wait - ожидание условия

▪ wait_for, wait_until - ожидание условия заданное время или до заданного момента

▪ notify_one - сообщить одному потоку

▪ notify_all - сообщить всем потокам

55

Условные переменные - производитель-потребитель

std::mutex mut;std::queue<Widget> widget_queue;std::condition_variable cond;

void producer() { for (;;) { Widget const w = get_request(); std::lock_guard<std::mutex> lock(mut); widget_queue.push(data); cond.notify_one();} }

void consumer() { for (;;) { std::unique_lock<std::mutex> lock(mut); cond.wait(lock, []{return !widget_queue.empty();}); Widget w = widget_queue.pop(); lock.unlock(); process(widget);} } 56

Будущие результаты

57

Будущие результаты (future)

std::future<> & std::shared_future<>58

Будущие результаты (future)

int thinking();

// Запуск асинхронной (“фоновой”) задачиstd::future<int> answer = std::async(thinking);

// Работа основного потокаdo_other_stuff(); // в этом время работает thinking()

// Получение результатовstd::cout << "The answer is " << answer.get() << std::endl;

T1main thread

работа ожидание

T2thinking...

answer.get()

async

59

Будущие результаты (future)

struct Widget { void foo(std::string const&, int); int bar(std::string const&); int operator()(int);};

Widget w;

// Вызывается foo("carpe dieum", 2014) для объекта wauto f1 = std::async(&Widget::foo, &w, "carpe diem", 2014);

// Вызывается bar("carpe dieum", 2014) для объекта tmp = wauto f2 = std::async(&Widget::bar, w, "carpe diem");

// Вызывается tmp.operator(2014), где tmp = wauto f3 = std::async(Widget(), 2014);

// Вызвается w(1234)auto f4 = std::async(std::ref(w), 2014);

60

Будущие результаты (future)

struct Widget { Widget(); Widget(Widget&&); // Конструктор перемещения Widget(Widget const&) = delete; // Запретить копирование

// Оператор “перемещающее присваивание” Widget& operator=(Widget&&);

// Запретить присваивание Widget& operator=(Widget const&) = delete;

void foo(std::string const&, int); int bar(std::string const&); int operator()(int);};

Widget w;auto f1 = std::async(&Widget::foo, &w, "hi", 2014);auto f2 = std::async(&Widget::bar, w, "hi");auto f3 = std::async(Widget(), 2014);auto f4 = std::async(std::ref(w), 2014);

61

Будущие результаты (future)

▪ std::launch::async - запуск функции в асинхронном режиме

▪ std::launch::deferred - запуск в момент вызова wait или get

▪ std::launch::async | std::launch::deferred - на усмотрение реализации (по умолчанию)

auto f5 = std::async(std::launch::deferred, Widget::foo(), "carpe diem", 2014);auto f6 = std::async(std::launch::deferred, Widget::bar(), "carpe diem");auto f7 = std::async(std::launch::async, Widget(), 2014);

std::cout << f5.get() << std::endl; // вызывается foo()f6.wait(); // вызывается bar()std::cout << f7.get() << std::endl; // только ожидание // результата

62

Упакованные задачи

task 1 task 2 task 3

package 1 package 2 package 3

63

Упакованные задачи

▪ Шаблон std::packaged_task<> связывается будущий результат (future) с функцией

▪ Вызов функции происходит при вызове объекта packaged_task

▪ Параметр шаблона - сигнатура функции

template<> class packaged_task<int(float, char)> {public: template<typename Callable> explicit packaged_task(Callable &func); std::future<int> get_future(); void operator()(std::vector<char>*, int);};

пример спецификации шаблона для сигнатуры функции int func(float, char)

64

Упакованные задачи - пример (пул задач)

task

package

task

package

tasks.push_back( std::move(task));

std::packaged_task<void()> task = std::move(tasks.front());

batch_systemadd_task

65

task()

Упакованные задачи - пример

std::mutex mut;std::deque<std::packaged_task<void()>> tasks;bool exit_flag = false;

bool is_exit() { std::mutex mut; std::lock_guard<std::mutex> lock(mut); return exit_flag;}

void batch_system() { while (!is_exit()) { std::unique_lock<std::mutex> lock(mut); if (tasks.empty()) continue; std::packaged_task<void()> task = // получить упакованную std::move(tasks.front()); // задачу из очереди tasks.pop_front(); // удалить из очереди lock.unlock(); task(); // запуск задачи} } 66

Упакованные задачи - пример

template<typename func>std::future<void> add_task(func f){ std::packaged_task<void()> task(f); std::future<void> res = task.get_future(); std::lock_guard<std::mutex> lock(mut); tasks.push_back(std::move(task));

return res;}

void say_vox() { std::cout << "vox\n"; }void say_populi() { std::cout << "populi\n"; }void say_dei() { std::cout << "dei\n"; }void write_word() { std::string s; std::cin >> s; }

67

Упакованные задачи - пример

int main() { std::thread batch(batch_system);

add_task(say_vox); add_task(say_populi); add_task(write_word); add_task(say_vox); add_task(say_dei);

std::this_thread::sleep_for( std::chrono::milliseconds(1000));

std::mutex mut; std::unique_lock<std::mutex> lock(mut); exit_flag = true; lock.unlock(); batch.join();

return 0;}

68

Упакованные задачи - пример, возможные варианты

$ ./prog voxpopuli

,

$ ./prog voxpopuli

,voxdei

$ ./prog voxpopuli

,vox

69

“Обещанные” результаты (std::promise)

std::futurestd::promise<...> p

I'm waiting...p.get_future().wait()

70

“Обещанные” результаты (std::promise)

std::futurestd::promise<...> p

Msg received!p.get_future().wait()

ok, let’s move!

p.set_value(msg)

71

“Обещанные” результаты (std::promise) - пример 1

void print_value(std::future<int>& fut) { int x = fut.get(); std::cout << "value: " << x << std::endl;}

int compute_value() { std::this_thread::sleep_for(std::chrono::seconds(1)); return 42;}

int main () { std::promise<int> prom;

// Получаем объект future из созданного promise (обещаем) std::future<int> fut = prom.get_future(); // Отправляем будущее значение в новый поток std::thread th1 (print_value, std::ref(fut));

int val = compute_value(); prom.set_value(val); // Выполняем обещание th1.join();} 72

“Обещанные” результаты (std::promise) - пример 1

mainmain thread

th1print_value

prom.set_value()

print_value

th1(print_value, std::ref(fut))

fut.get()

compute_value

работа ожидание

создание/завершениепотоков синхронизация

73

“Обещанные” результаты (std::promise) - пример 2

int main() { std::istringstream iss_numbers{"3 1 42 23 -23 93 2 -289"}; std::istringstream iss_letters{" a 23 b,e k k?a;si,ksa c"};

std::vector<int> numbers; std::vector<char> letters; std::promise<void> numbers_promise, letters_promise;

auto numbers_ready = numbers_promise.get_future(); auto letter_ready = letters_promise.get_future();

std::thread value_reader([&]{ std::copy(std::istream_iterator<int>{iss_numbers}, std::istream_iterator<int>{}, std::back_inserter(numbers));

numbers_promise.set_value();

std::copy_if(std::istreambuf_iterator<char>{iss_letters}, std::istreambuf_iterator<char>{}, std::back_inserter(letters), ::isalpha); letters_promise.set_value(); }); 74

“Обещанные” результаты (std::promise) - пример 2

numbers_ready.wait(); // Ждать когда числа будут готовы

std::sort(numbers.begin(), numbers.end());

if (letter_ready.wait_for(std::chrono::milliseconds(100)) == std::future_status::timeout) { // выводим числа, пока обрабатываются символы for (int num : numbers) std::cout << num << ' '; std::cout << '\n'; numbers.clear(); // Числа уже были напечатаны }

letter_ready.wait(); std::sort(letters.begin(), letters.end());

for (char let : letters) std::cout << let << ' '; std::cout << '\n';

// If numbers were already printed, it does nothing. for (int num : numbers) std::cout << num << ' '; std::cout << '\n';

value_reader.join();}

75

“Обещанные” результаты (std::promise) - пример 2

mainmain

работа ожидание

value_reader

letters_promise.set_value()value_reader

fut.get()

iss_numbers iss_letters

number_ready.wait()

sort

letter_ready.wait_for

sort output

numbers_promise.set_value()

создание/завершениепотоков синхронизация

76

“Обещанные” результаты (std::promise), варианты

a a a a b c e i k k k s s -289 -23 1 2 3 4 23 42 93 93

-289 -23 1 2 3 23 42 93 a a a b c e i k k k s s

77

Полезные советы

78

Используйте задачи, а не потоки

79

Проблемы с параллелизмом на основе потоков

int doWork();

std::thread t(doWork); // 1// илиauto fut = std::async(doWork); // 2

80

▪ Вариант, основанный на задаче (2), предпочтительней, т.к. предполагает возвращаемое значение, которое можно получить fut.get().

▪ Если doWork выбрасывает исключение, то get() позволяет обработать исключения, в то время как в первом случае выброс исключения приведёт к завершению программы.

Проблемы с параллелизмом на основе потоков

81

Параллелизм задач находится на более высоком уровне абстракции по сравнению с параллелизмом потоков, освобождает программиста от деталей реализации:▪ Аппаратные потоки (software threads) - те, которые

действительно выполняют вычисления (по числу ядер).▪ Программные потоки (hardware threads) - потоки,

которые планируются ОС и выполняются на аппаратных потоках. ▫ Легковесные потоки (lightweight threads) - потоки,

которые выполняются целиком в пространстве пользователя.

▪ std::thread - объекты С++, которые соответствуют определённым программным потокам, с которыми можно выполнять операции join и detach

Ограниченность количества программных потоков

82

Программные потоки - ограниченный ресурс. Попытка создать больше заданного числа потоков вызовет исключение, даже если

int doWork() noexcept;

std::thread t(doWork); // может быть исключение!

▪ Запустить doWork в текущем потоке?▪ Или подождать, пока освободится программный поток?

:(

Перегруженность аппаратных потоков (oversubscription)

83

Состояние перегруженности аппаратных потоков oversubscription возникает, когда в системе большое количество runnable-потоков. Планировщик ОС выделяет программным потокам порции (time-slice) процессорного времени. После окончания порции происходит переключение контекста (context switch), особенно в случае, когда поток назначается на разные ядра:▪ Кэш-память не загружена, большое количество

промахов по кэшу.▪ Запуск нового потока на ядре перезаписывает записи

для старого потока, который, вероятно, будет опять назначен на это ядро. Это опять приводит к промахам по кэшу.

Перегруженность аппаратных потоков (oversubscription)

84

Выбор оптимального количества потоков для избежания перегруженности зависит от:▪ Момента, когда программа переходит из региона с

вводом-выводом к области с вычислениями.▪ Стоимости переключения контекста▪ Того, насколько эффективно потоки используют кэш▪ Аппаратной архитектуры

Перегруженность аппаратных потоков (oversubscription)

85

Выбор оптимального количества потоков для избежания перегруженности зависит от:▪ Момента, когда программа переходит из региона с

вводом-выводом к области с вычислениями.▪ Стоимости переключения контекста▪ Того, насколько эффективно потоки используют кэш▪ Аппаратной архитектуры

Сделай жизнь легче, используй std::async!Ответственность за управление потоками лежит на плечах разработчика стандартной библиотеки!

Long live std::async!

86

std::async позволяет создать неограниченное количество асинхронных функций▪ Вызов std::async не гарантирует создание нового

программного потока (политики async и deferred).▪ Асинхронная функция может быть запущена, например,

в том же потоке, где и вызывается get, wait,позволяя избежать перегруженности (oversubscription).

▪ Возможность work-stealing (легковесных потоков)

Long live std::async! But...

87

std::async позволяет создать неограниченное количество асинхронных функций▪ Вызов std::async не гарантирует создание нового

программного потока (политики async и deferred).▪ Асинхронная функция может быть запущена, например,

в том же потоке, где и вызывается get, wait,позволяя избежать перегруженности (oversubscription).

▪ Возможность work-stealing (легковесных потоков)Но std::async не универсальное средство. Недостатки:▪ Приводит к возможному дисбалансу загрузки.

Планирование происходит на двух уровнях: ОС и программы.

▪ Не подходит для некоторых целей (напр., GUI)

Когда таки нужно использовать потоки

88

▪ Нужно воспользоваться функционалом низкоуровневой реализации потоков (например, std::thread::native_handle)

▪ Нужно оптимизировать использвоание потоков в программе. Допустим, если вы разрабатываете сервер, который будет запускаться на заданной архитектуре.

▪ Вы хотите реализовать потоки на архитектуре, где пока нет реализации C++-concurrency.

Но это редко. В большинстве случаев смело разрабатывайте программы на основе задач, а не на основе потоков.

Быстрая сортировка в духе функционального прог-я

89

Быстрая сортировка в духе функционального прог-я

template<typename T>std::list<T> sequential_quick_sort(std::list<T> input) { std::list<T> result; result.splice(result.begin(), input, input.begin()); T const &pivot = *result.begin();

auto divide_point = std::partition(input.begin(), input.end(), [&](T const& t){return t < pivot; });

std::list<T> lower_part; lower_part.splice(lower_part.end(), input, input.begin(), divide_point); auto new_lower( sequential_quick_sort(std::move(lower_part))); auto new_higher( sequential_quick_sort(std::move(input)));

result.splice(result.end(), new_higher); result.splice(result.begin(), new_lower); return result; }

90

Быстрая сортировка в духе функционального прог-я

lowerpart

input

input

pivot

splice

divide_point

new_lower

new_higher

91

Параллельный алгоритм быстрой сортировки

template<typename T>std::list<T> parallel_quick_sort(std::list<T> input) { std::list<T> result; result.splice(result.begin(), input, input.begin()); T const &pivot = *result.begin();

auto divide_point = std::partition(input.begin(), input.end(), [&](T const& t){return t < pivot; });

std::list<T> lower_part; lower_part.splice(lower_part.end(), input, input.begin(), divide_point); std::future<std::list<T>> new_lower( std::async(parallel_quick_sort, std::move(lower_part))); auto new_higher( parallel_quick_sort(std::move(input)));

result.splice(result.end(), new_higher); result.splice(result.begin(), new_lower.get()); return result; }

92

Учитывайте политику запуска задачи

93

Помните про политику запуска задачи

94

▪ std::launch::async - функция будет запущена асинхронного, т.е. в отдельном потоке

T1main thread

T2func

fut.get()

▪ std::launch::deferred - функция может быть запущена только, когда вызан методв get или wait для объекта future в потоке, вызывающем get (wait).

T1main thread

T2func

fut.get()

async

async

Помните про политику запуска задачи

95

▪ std::launch::async - функция будет запущена асинхронного, т.е. в отдельном потоке

▪ std::launch::deferred - функция может быть запущена только, когда вызан методв get или wait для объекта future в потоке, вызывающем get (wait).

auto fut = std::async(func); // использовать политику // запуска по умолчанию

▪ Нельзя предугадать, будет ли func выполняться асинхронно

▪ Нельзя предугадать, будет ли func запущена на потоке, отличном, от потока, вызывающего get (wait)

▪ Нельзя предугадать, что func будет выполнена.

Проблема: std::async и Thread Local Storage (TLS)

96

auto fut = std::async([](){ // Может использоваться thread_local local_var; // TLS для независимого ... // потока});...fut.get(); // а может и для этого!

Политика запуска по умолчанию конфликтует с использованием переменных thread_local:

Проблема: std::async и цикла на основе wait_for

97

using namespace std::literals; // C++14 суффиксыauto fut = std::async([]() { std::this_thread::sleep_for(1s);});

// Цикл, ожидающий выполнения std::async, // может не завершитьсяwhile (fut.wait_for(100ms) != std::future_status::ready){ /* делать что-то асинхронного */}

Использование циклов на основе вызова wait_for или wait_until может привести к вечному ожиданию, если задача будет запущена как отложенная (std::launch::deferred):

Проблема: std::async и цикла на основе wait_for - решение

98

auto fut = std::async([]() { std::this_thread::sleep_for(1s);});

if (fut.wait_for(0s) == std::future_satus::deferred) { fut.get(); // ожидаем результата ...} else { while (fut.wait_for(100ms) != std::future_status::ready) { // делать какую-то работу асинхронного, // пока ждём завершения выполнения задачи } // здесь fut готово}

Когда использовать политику запуска по умолчанию

99

▪ Задача не требует асинхронного запуска в отдельном потоке, отличном от вызывающего get (wait).

▪ Не важно, thread_local-переменные какого потока будут использоваться.

▪ Или есть гарантия, что get (wait) будут вызваны для объекта future, возвращённого std::async, или задача может быть вовсе на запущена.

▪ При использовании wait_for или wait_until допускается возможность отложенного запуска задачи.

Если какие-либо пункты не выполняются, лучше гарантировать асинхронный запуск задачи через передачу std::launch::async

Используйте потоки, не требующие вызова join (unjoinable)

100

Joinable и unjoinable

101

Объект std::thread может пребывать в двух состояниях:▪ joinable: объект соответствует потоку, который

выполняется или может быть запущен.▪ unjoinable: объект, с которым нельзя выполнить

операцию join:▫ выполнен конструктур по умолчанию для std::

thread, т.е. std::thread может не имеет функции для выполнения и поэтому не соответствует реальному потоку.

▫ std::thread, который был перемещён (moved)▫ std::thread, который был присоединён (joined)▫ std::thread, который был отсоденинён (detached)

Проблема с joinable-потоками

102

constexpr auto n = 10'000'000; // C++14-style

bool doWork(std::function<bool(int)> pred, // условие int maxVal = n) {

std::vector<int> goodVals; // значения, удовл. условию

std::thread t([&pred, maxVals, &goodVals]{ for (auto i = 0; i <= maxVals; i++) { if (pred(i)) goodVals.push_back(i); } });

auto nh = t.native_handle(); ... // низкоуровневые манипуляции с потоком

if (conditionAreSatisfied()) { t.join(); performComputation(goodVals); return true; } return false;}

Проблема с joinable-потоками

103

constexpr auto n = 10'000'000; // C++14-style

bool doWork(std::function<bool(int)> pred, // условие int maxVal = n) {

std::vector<int> goodVals; // значения, удовл. условию

std::thread t([&pred, maxVals, &goodVals]{ for (auto i = 0; i <= maxVals; i++) { if (pred(i)) goodVals.push_back(i); } });

auto nh = t.native_handle(); ...

if (conditionAreSatisfied()) { t.join(); performComputation(goodVals); return true; } return false;}

sched_param sch;int policy;pthread_getschedparam(nh, &policy, &sch);sch.sched_priority = 20;

if (pthread_setschedparam(nh, SCHED_FIFO, &sch)) { std::cout << "Failed to setschedparam: " << std::strerror(errno) << '\n';}

Проблема с joinable-потоками

constexpr auto n = 10'000'000; // C++14-style

bool doWork(std::function<bool(int)> pred, // условие int maxVal = n) {

std::vector<int> goodVals; // значения, удовл. условию

std::thread t([&pred, maxVals, &goodVals]{ for (auto i = 0; i <= maxVals; i++) { if (pred(i)) goodVals.push_back(i); } });

auto nh = t.native_handle(); ...

if (conditionAreSatisfied()) { t.join(); performComputation(goodVals); return true; // ok, т.к. был t.join() } return false; // не было t.join()! выброс исключения} // и аварийное завершение программы 104

Проблема с joinable-потоками

Как можно решить проблему (наивно):▪ Неявный join. Деструктор std::thread будет ожидать

завершения потока. Но это приведёт к неочевидному коду, например, когда поток ждёт завершения doWork, уже зная, что условие не выполнено.

▪ Неявный detach. Деструктор разрывает связь между std::thread и потоком выполнения. Поток продолжает работать. В этом случае, например, при завершении функции doWork, поток продолжает работать. Этот поток может использовать автоматические переменные из стека doWork.

Поэтому стандарт запретил уничтожение потока в сотоянии joinable: деструктур такого объекта вызывает завершение программы.

105

Проблема с joinable-потоками

106

▪ Программист должен следить за тем, что объект должен находиться в состоянии unjoinable вне его области видимости.

▪ Обеспечение этого требования - задача непростая, поскольку требует отслеживания всех выходов из функции через return, continue, break, goto, exception.

▪ Необходимо обеспечить выполнение определённого действия каждый раз при выходе из блока.

Make it RAII!

▪ Программист должен следить за тем, что объект должен находиться в состоянии unjoinable вне его области видимости.

▪ Обеспечение этого требования - задача непростая, поскольку требует отслеживания всех выходов из функции через return, continue, break, goto, exception.

▪ Необходимо обеспечить выполнение определённого действия каждый раз при выходе из блока.

Решение: RAII-объекты (Resouce Acquisition Is Initialization), (к которым относятся std::unique_ptr, std::shared_ptr, std::lock_guard, std::fstream и др.), деструктор которых содержит необходимое действие.

107

RAII

108

class ThreadRAII {public: enum class DestrAction { join, detach }; ThreadRAII(std::thread&& t, DestrAction a): action{a}, t{std::move(t)} { } ~ThreadRAII() { // действие выполняется в деструкторе if (t.joinable()) { if (action == DestrAction::join) { t.join(); } else { t.detach(); } } } std::thread& get() { return t; }private: DestrAction action; // action in destuctor std::thread t;};

RAII

109

bool doWork(std::function<bool(int)> pred, int maxVal = n) {

std::vector<int> goodVals; // значения, удовл. условию

ThreadRAII t{ // использовать RAII-объект std::thread([&stencil, maxVals, &goodVals]{ for (auto i = 0; i <= maxVals; i++) { if (pred(i)) goodVals.push_back(i); } }), ThreadRAII::DestrAction::join // действие }; // в деструкторе

auto nh = t.get().native_handle(); ...

if (conditionAreSatisfied()) { t.get().join(); performComputation(goodVals); return true; } return false;}

Используйте future-promise для синхронизации потоков (а не cond vars)

110

“Обещанные” результаты (std::promise)

Msg received!

ok, let’s move!

cv.notify_one()

111

Условные переменные?

std::unique_lock<std::mutex> lk(m);cv.wait(lk)

cv

Недостатки синхр-ции на основе условных переменных

std::mutex mut;std::queue<Widget> widget_queue;std::condition_variable cond;

void producer() { for (;;) { Widget const w = get_request(); std::lock_guard<std::mutex> lock(mut); widget_queue.push(data); cond.notify_one();} }

void consumer() { for (;;) { std::unique_lock<std::mutex> lock(mut); cond.wait(lock, []{return !widget_queue.empty();}); Widget w = widget_queue.pop(); lock.unlock(); process(widget);} }

112

Недостатки синхр-ции на основе условных переменных

▪ Необходимость использования мьютексаstd::unique_lock<std::mutex> lock(mut);

cond.wait(lock, ...);

А что, если потоки выполняют код, который не нуждается в блокировке мьютекса? Например, один поток инициализирует структуру, после чего сообщает другому, что структура готова.

▪ Пропущенный сигналПоток может отправить сигнал (notify_one/all) тогда, когда другой поток ещё не начал его ожидать.

▪ Ложное пробуждение (spurious wakeup)Поток может проснуться тогда, когда сигнал не был отправлен Или когда он был отправлен потока, а затем условие перестало выполняться. Поэтому нужна дополнительная проверка:

cond.wait(lock, []{return !widget_queue.empty();}));

А что, если поток не может проверить условие?!113

Недостатки синхр-ции на основе условных переменных

std::atomic<bool> flag(false);...flag = true;...while (!flag); // активное ожидание! :(...

114

Для решения проблемы ложного пробуждения можно использовать атомарный флаг:

Или так:

{ flag = true; cv.notify_one();}{ cv.wait(lk, [] { return flag; }); // код “с запашком” :(}

“Обещанные” результаты (std::promise)

std::futurestd::promise<...> p

p.get_future().wait()

p.set_value()

115

ok, let’s move!

“Обещанные” результаты (std::promise)

std::futurestd::promise<...> p

p.get_future().wait()

p.set_value()

116

ok, let’s move!

▪ Не требует мьютексов▪ Не нуждается в атомарных флагах▪ Не использует активного ожидания▪ Не зависит от порядка выполнения wait, set_value

“Обещанные” результаты (std::promise)

std::futurestd::promise<...> p

p.get_future().wait()

p.set_value()

117

ok, let’s move!

▪ Не требует мьютексов▪ Не нуждается в атомарных флагах▪ Не использует активного ожидания▪ Не зависит от порядка выполнения wait, set_value

▪ Надо заботиться о поведении деструктора future▪ Можно отправить сигнал только один раз

“Обещанные” результаты (std::promise) - пример

std::promise<void> p;

void react(); // реакция на условие

void detect() { // обнаружение условия std::thread t([] { p.get_future().wait(); react(); });

// делаем что-то // в это время t спит

p.set_value(); // разбудить t

// делаем ещё что-то

t.join();};

118

“Обещанные” результаты (std::promise) - пример

std::promise<void> p;

void react(); // реакция на условие

void detect() { // обнаружение условия std::thread t([] { p.get_future().wait(); react(); });

// а что, если здесь возникнет исключение?? p.set_value(); // разбудить t // делаем ещё что-то t.join();};

119

“Обещанные” результаты (std::promise) - пример

std::promise<void> p;

void react(); // реакция на условие

void detect() { // обнаружение условия ThreadRAII t(std::thread([] { p.get_future().wait(); react(); )});

...

p.set_value(); // разбудить t // делаем ещё что-то t.join();};

120

Множественная отправка сигналов

std::promise<void> p;

void detect() {

auto sf = p.get_future().share();

std::vector<std::thread> vt;

for (auto i = 0; i < threadsToRun; i++) { vt.emplace_back([sf]{ sf.wait(); react(); }); }

// ...

p.set_value();

// ...

for (auto &t: vt) t.join();};

121

Множественная отправка сигналов

std::promise<void> p;

void detect() {

auto sf = p.get_future().share();

std::vector<std::thread> vt;

for (auto i = 0; i < threadsToRun; i++) { vt.emplace_back([sf]{ sf.wait(); react(); }); }

// ...

p.set_value();

// ...

for (auto &t: vt) t.join();};// RAII... 122

Множественная отправка сигналов

std::promise<void> p;

void detect() { auto sf = p.get_future().share(); std::vector<ThreadRAII> vt;

for (auto i = 0; i < nthreads; i++) { vt.emplace_back(std::move(ThreadRAII{ std::thread([sf]{ sf.wait(); react(); } ), ThreadRAII::DestrAction::join })); }

// ...

p.set_value();};

123

Разделяемые будущие результаты shared_future

int main() { std::promise<void> ready_promise, t1_ready_promise, t2_ready_promise; std::shared_future<void> ready_future(ready_promise.get_future()); std::chrono::time_point<std::chrono::high_resolution_clock> start;

auto fun1 = [&]() -> std::chrono::duration<double, std::milli> { t1_ready_promise.set_value(); ready_future.wait(); // ожидать сигнала из main() return std::chrono::high_resolution_clock::now() - start; };

auto fun2 = [&]() -> std::chrono::duration<double, std::milli> { t2_ready_promise.set_value(); ready_future.wait(); // ожидать сигнала из main() return std::chrono::high_resolution_clock::now() - start; }; 124

Разделяемые будущие результаты shared_future

auto result1 = std::async(std::launch::async, fun1); auto result2 = std::async(std::launch::async, fun2);

// ждать, пока потоки не будут готовы t1_ready_promise.get_future().wait(); t2_ready_promise.get_future().wait();

// потоки готовы - начать отчёт времени start = std::chrono::high_resolution_clock::now();

// запустить потоки ready_promise.set_value();

std::cout << "Thread 1 received the signal " << result1.get().count() << " ms after start\n" << "Thread 2 received the signal " << result2.get().count() << " ms after start\n";}

125

Разделяемые будущие результаты shared_future

main

работа ожидание

создание/завершениепотоков синхронизация

T1

T2t2_ready.set_value

start

return

return

output

ready.set_value

t1_ready.set_value

126

Учитывайте поведение деструктора объекта future

127

Хранение результата для future

Деструктор объекта future ведёт себя иногда так, как будто он выполняет неявный join, а в некоторых случае - как будто выполняет неявный detach.

128

Вызываемый поток

Вызывающий поток

future std::promise

Где хранится результат вызывающего потока?Вызывающий поток может завершиться до того, как вызываемый выполнит fut.get(), и результат не может храниться в объекте std::promise вызываемого потока.Объект future не может быть хранилищем для результата, т.к. он может быть скопирован в объекты shared_future, после чего возникает вопрос, какая из копий соответствует результату?

Два варианта поведения деструктора future

129

Вызываемый поток

Вызывающий поток

future std::promise

Результат вызывающего

Поведение деструктора future зависит от разделяемого состояния (shared state): ▪ Деструктор последнего объекта future,

указывающего на разделяемое состояние (shared state) для какой-то асинхронной задачи, блокируется до завершения выполнения этой задачи, т.е. выполняет “join”.

▪ Деструкторы всех других объектов future просто уничтожают объект future. Это аналогично вызову detach для потока.

shared state

Два варианта поведения деструктора future

130

▪ Деструктор последнего объекта future, указывающего на разделяемое состояние, выполняет “join”, если:▫ объект указывает на разделяемое состояние,

созданное std::async▫ задача, породившая future, была запущена

асинхронно▫ future - это последний объект future, указывающий

на разделяемое состояниеЗачем это нужно? ▪ Чтобы избежать неявного вызова detach для потока, в

котором выполняется задача.▪ Срабатывание деструктора не должно приводить к

завершению программы (попытка компромиса)

Два варианта поведения деструктора future

131

// Деструктор futs может блокироватьсяstd::vector<std::future<void>> futs;

// Объект может блокироваться при уничтоженииclass Widget {private: std::shared_future<double> fut;};

Два варианта поведения деструктора future - пример

132

auto fut1 = std::async(std::launch::async, [] { std::this_thread::sleep_for(1s); std::cout << "1st task finished\n";});

auto fut2 = std::async(std::launch::async, [](auto fut2) { return "2nd task finished\n";}, std::move(fut1));

std::cout << fut2.get();

Два варианта поведения деструктора future - пример

133

auto fut1 = std::async(std::launch::async, [] { std::this_thread::sleep_for(1s); std::cout << "1st task finished\n";});

auto fut2 = std::async(std::launch::async, [](auto fut2) { return "2nd task finished\n";}, std::move(fut1));

std::cout << fut2.get();

$ ./prog 1st task finished2nd task finished

Два варианта поведения деструктора future - пример

134

auto fut1 = std::async(std::launch::async, [] { std::this_thread::sleep_for(1s); std::cout << "1st task finished\n";}).share();

auto fut2 = std::async(std::launch::async, [](auto fut2) { return "2nd task finished\n";}, fut1);

std::cout << fut2.get();

$ ./prog 2nd task finished1st task finished

Два варианта поведения деструктора future - пример

135

auto fut1 = std::async(std::launch::async, [] { std::this_thread::sleep_for(1s); std::cout << "1st task finished\n";}).share();

auto fut2 = std::async(std::launch::async, [](auto fut2) { return "2nd task finished\n";}, fut1);

std::cout << fut2.get();

// Деструктор futs может блокироватьсяstd::vector<std::future<void>> futs;

// Объект может блокироваться при уничтоженииclass Widget {private: std::shared_future<double> fut;};

Неблокирующие будущие результаты и другие перспективные примитивы синхронизации

136

Неблокирующие будущие результаты (then)

auto func1() { std::cout << "begin thinking over the answer...\n"; std::this_thread::sleep_for(dur3); return 40;}

auto func2(int x) { std::cout << "continue thinking over the answer...\n"; std::this_thread::sleep_for(dur1); return x + 2;}

auto func3(int x) { std::cout << "still thinking...\n"; std::this_thread::sleep_for(dur2); return "number " + std::to_string(x);}

void do_some_stuff() { std::cout << "do some useful stuff"; }

void do_some_other_stuff() { std::cout << "do other stuff"; }137

Неблокирующие будущие результаты (then)

int main() { auto f1 = std::async(func1); auto f2 = std::async(func2, f1.get()); auto f3 = std::async(func3, f2.get());

std::cout << "waiting for the answer...\n";

do_some_stuff();

std::cout << "answer: " << f3.get() << std::endl;

do_some_other_stuff();

138

Неблокирующие будущие результаты (then)

int main() { auto f1 = std::async(func1); auto f2 = std::async(func2, f1.get()); auto f3 = std::async(func3, f2.get());

std::cout << "waiting for the answer...\n";

do_some_stuff();

std::cout << "answer: " << f3.get() << std::endl;

do_some_other_stuff();

Каждый раз после получения результата выполняется создание новой асинхронной задачи.

Поток может быть заблокирован при вызове get() для ожидания результата.

139

Неблокирующие будущие результаты (then)

$ ./prog begin thinking over the answer...continue thinking over the answer...waiting for the answer...do some useful stuffanswer: still thinking...number 42do some other useful stuff

140

Неблокирующие будущие результаты (then)

#define BOOST_THREAD_PROVIDES_FUTURE#define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION#include <boost/thread/future.hpp>

int main() { auto f = boost::async([](){ return func1(); });

do_some_stuff();

f.then([](auto f){ std::cout << "answer: " << f.get() << std::endl; });

do_some_other_stuff();

141

вызывающий поток блокируется

Неблокирующие будущие результаты (then)

#define BOOST_THREAD_PROVIDES_FUTURE#define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION#include <boost/thread/future.hpp>

int main() { auto f = boost::async([](){ return func1(); }).then([](auto f){ return func2(f.get()); }).then([](auto f){ return func3(f.get()); });

std::cout << "waiting for the answer...\n";

do_some_stuff();

f.then([](auto f){ std::cout << "answer: " << f.get() << std::endl; });

do_some_other_stuff(); 142

Неблокирующие будущие результаты (then)

#define BOOST_THREAD_PROVIDES_FUTURE#define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION#include <boost/thread/future.hpp>

int main() { auto f = boost::async([](){ return func1(); }).then([](auto f){ return func2(f.get()); }).then([](auto f){ return func3(f.get()); });

std::cout << "waiting for the answer...\n";

do_some_stuff();

f.then([](auto f){ std::cout << "answer: " << f.get() << std::endl; });

do_some_other_stuff();

вызывающий поток не блокируется143

Неблокирующие будущие результаты (then)

#define BOOST_THREAD_PROVIDES_FUTURE#define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION#include <boost/thread/future.hpp>

int main() { auto f = boost::async([](){ return func1(); }).then([](auto f){ return func2(f.get()); }).then([](auto f){ return func3(f.get()); });

std::cout << "waiting for the answer...\n";

do_some_stuff();

f.then([](auto f){ std::cout << "answer: " << f.get() << std::endl; }).wait();

do_some_other_stuff();

вызывающий поток блокируется

144

Неблокирующие будущие результаты (then)

$ g++ -Wall -pedantic -pthread -lboost_system \ -lboost_thread -std=c++14 -O2 prog.cpp -o prog

$ ./progwaiting for the answer...do some useful stuffbegin thinking over the answer...continue thinking over the answer...still thinking...answer: number 42do some other useful stuff

145

Неблокирующие будущие результаты (then)

Блокирующие future Неблокирующие future

f2

f3

f1

f

▪ устанавливается явный порядок выполнения

▪ нет блокировок

▪ поток один

▪ порядок выполнения неопределён

▪ возможны блокировки

▪ для каждой задачи создаётся отдельный поток 146

Ожидание выполнения всех задач (when_all)

f2

f1f3

Будущий результат f4 зависит от выполнения всех будущих результатов f1, f2, f3 и начинает выполняться после завершения выполнения задач, им соответствующих (подобно барьерной синхронизации).

f4

147

Ожидание выполнения всех задач (when_all)

#define BOOST_THREAD_PROVIDES_FUTURE_WHEN_ALL_WHEN_ANY#include <boost/thread/future.hpp>

std::vector<boost::future<void>> task_chunk;

task_chunk.emplace_back(boost::async([]() { std::cout << "hello from task 1\n"; }));task_chunk.emplace_back(boost::async([]() { std::cout << "hello from task 2\n"; }));task_chunk.emplace_back(boost::async([]() { std::cout << "hello from task 3\n"; }));

auto join_task = boost::when_all(task_chunk.begin(), task_chunk.end());

do_some_stuff();

join_task.wait();

148

Ожидание выполнения всех задач (when_all)

std::vector<boost::future<int>> task_chunk;

task_chunk.emplace_back(boost::async(boost::launch::async, [](){ std::cout << "hello from task 1\n"; return 10; }));

task_chunk.emplace_back(boost::async(boost::launch::async, [](){ std::cout << "hello from task 2\n"; return 20; }));

task_chunk.emplace_back(boost::async(boost::launch::async, [](){ std::cout << "hello from task 3\n"; return 12; }));

auto join_task = boost::when_all(task_chunk.begin(), task_chunk.end()) .then([](auto results){ auto res = 0; for (auto &elem: results.get()) res += elem.get(); return res; });

do_some_stuff();

std::cout << "result: " << join_task.get() << std::endl;

join_task имеет тип

future< vector< future<T>>>

149

Ожидание выполнения всех задач (when_all)

f2

f1f3

f4

f5

150

Ожидание выполнения всех задач (when_all)

$ g++ -Wall -pedantic -pthread -lboost_system \ -lboost_thread -std=c++14 -O2 prog.cpp -o prog

$ ./prog hello from task 1hello from task 3hello from task 2do some useful stuffresult: 42

151

Ожидание выполнения какой-либо задачи (when_any)

f2

f1f3

Будущий результат f4 зависит от выполнения одного из будущих результатов f1, f2, f3 и начинает выполняться после завершения выполнения хотя бы одной задачи (подобно синхронизации “эврика”).

f4

152

Ожидание выполнения какой-либо задачи (when_any)

std::vector<boost::future<decltype(M_PI)>> task_chunk;

task_chunk.emplace_back(boost::async(boost::launch::async, []() { std::this_thread::sleep_for(dur1); return M_PI; }));

task_chunk.emplace_back(boost::async(boost::launch::async, []() { std::this_thread::sleep_for(dur2); return M_E; }));

task_chunk.emplace_back(boost::async(boost::launch::async, []() { std::this_thread::sleep_for(dur3); return M_LN2; }));

auto join_task = boost::when_any(task_chunk.begin(), task_chunk.end()) .then([](auto results) { for (auto &elem: results.get()) { if (elem.is_ready()) { return elem.get(); } } exit(1); // this will never happen });

do_some_stuff();

std::cout << "result: " << join_task.get() << std::endl;153

join_task имеет тип

future< vector< future<T>>>

Ожидание выполнения какой-либо задачи (when_any)

$ g++ -Wall -pedantic -pthread -lboost_system \ -lboost_thread -std=c++14 -O2 prog.cpp -o prog

do some useful stuffresult: 2.71828

do some useful stuffresult: 0.693147

do some useful stuffresult: 3.14159

154

Возможные варианты:

Реализация высокоуровневых средств синхронизации

155

Мьютексы и очереди задач

class Logger {

std::fstream flog;

public:

void writelog(...) {

flog << current_time()

<< ":" << logmsg

<< std::endl;

}

};

class Logger {

std::fstream flog;

public:

void writelog(...) {

flog << current_time()

<< ":" << logmsg

<< std::endl;

}

};

Очереди задачБлокировка мьютекса

156

Мьютексы и очереди задач

class Logger {

std::fstream flog;

std::mutex mut;

public:

void writelog(...) {

std::lock_guard

<std::mutex> lock(mut);

flog << current_time()

<< ":" << logmsg

<< std::endl;

}

};

class Logger {

std::fstream flog;

worker_thread worker;

public:

void writelog(...) {

worker.send([=]{

flog << current_time()

<< ":" << logmsg

<< std::endl;

});

}

};

Блокировка мьютекса Очереди задач

157

Мьютексы и очереди задач

Блокировка мьютекса

▪ потоки блокируются

▪ имеется возможность дедлока

▪ небольшая масштабируемость

▪ порядок следования сообщения в логе отличается от последовательности поступления

Очереди задач

▪ потоки не блокируются

▪ отсутствует возможность дедлока

▪ высокая масштабируемость

▪ порядок следования сообщения в логе совпадает с фактическим

158

Паттерн: потокобезопасная обёртка над данными

Требования к потокобезопасным “обёрткам”:

1. Сохранение интерфейсаwidget w; => w.func("hi folks!");wrapper<widget> w; => w.func("hi folks!");

2. Универсальность. Заранее может быть неизвестны методы, которые необходимо обернуть. Некоторые методы сложно обернуть: конструкторы, операторы, шаблоны и т.д.

3. Поддержка транзакцийaccount.deposit(Sergey, 1000)account.withdraw(Ivan, 1000);log.print("user ", username, "data ");log.print("time ", logmsg);

Реализация отдельных методов может не обеспечить необходимую гранулярность.

159

Паттерн: обёртка над данными с блокировками

template<typename T>class wrapper {private: T t; // оборачиваемый объект ... // состояние враппера

public: monitor(T _t): t(_t) { }

template <typename F>

// 1. получаем любую функцию // 2. подставляем в неё оборачиваемый объект // 3. выполняем и возвращаем результат auto operator()(F f) -> decltype(f(t)) { // работа враппера auto ret = f(t); // ... return ret; }}; 160

Потокобезопасная обёртка над данными с блокировками

template<typename T>class monitor {private: T t; std::mutex m;

public: monitor(T _t): t(_t) { }

template <typename F> auto operator()(F f) -> decltype(f(t)) { std::lock_guard<std::mutex> lock(m);

// вызов “объявления” под защитой мьютекса return f(t); }};

161

Потокобезопасная обёртка над данными с блокировками

monitor<std::string> smon{"start "}; // инициализацияstd::vector<std::future<void>> v;

for (auto i = 0; i < 5; i++) { // выполнить асинхр. задачи... v.emplace_back(std::async(std::launch::async, [&, i]{

smon([=](auto &s){ // "объявление" функции s += "i = " + std::to_string(i); s += " "; });

smon([](auto &s){ // "объявление" функции std::cout << s << std::endl; }); }));}

for (auto &f: v) // дождаться завершения f.wait();

std::cout << "done\n";162

Потокобезопасная обёртка над данными с блокировками

start i = 1 start i = 1 i = 0 start i = 1 i = 0 i = 2 start i = 1 i = 0 i = 2 i = 4 start i = 1 i = 0 i = 2 i = 4 i = 3 done

start i = 0 start i = 0 i = 2 start i = 0 i = 2 i = 3 start i = 0 i = 2 i = 3 i = 1 start i = 0 i = 2 i = 3 i = 1 i = 4 done

start i = 0 start i = 0 i = 2 start i = 0 i = 2 i = 4 start i = 0 i = 2 i = 4 i = 1 start i = 0 i = 2 i = 4 i = 1 i = 3done 163

Потокобезопасная обёртка над данными с блокировками

monitor<std::ostream&> mon_cout{std::cout};std::vector<std::future<void>> v;

for (auto i = 0; i < 5; i++) { v.emplace_back(std::async(std::launch::async, [&, i]{ mon_cout([=](auto &cout){ cout << "i = " << std::to_string(i); cout << "\n"; }); mon_cout([=](auto &cout){ cout << "hi from " << i << std::endl; }); }));}

for (auto &f: v) f.wait();

mon_cout([](auto &cout){ cout << "done\n";}); 164

Потокобезопасная обёртка над данными с блокировками

i = 0i = 2hi from 2hi from 0i = 1hi from 1i = 3hi from 3i = 4hi from 4done

i = 0i = 3i = 2hi from 2i = 1hi from 1hi from 3i = 4hi from 4hi from 0done

i = 0hi from 0i = 4hi from 4i = 2hi from 2i = 3hi from 3i = 1hi from 1done

i = 0hi from 0i = 2hi from 2i = 3hi from 3i = 1hi from 1i = 4hi from 4done

165

Потокобезопасная обёртка над данными на основе очереди задач

template<typename T> class concurrent {private: // потокобезопасная очередь T t; concurrent_queue<std::function<void()>> q; bool done = false; std::thread thd;

public: concurrent(T t_): t{t_}, thd{[=]{ while (!done) { (*q.wait_and_pop())(); // дождаться поступления } // значения, извлечь } } { } // из очереди и выполнить

~concurrent() { q.push([=]{ done = true; }); thd.join(); }

template<typename F> void operator()(F f) { q.push([=]{ f(t); }); }}; 166

Потокобезопасная обёртка над данными на основе очереди задач

concurrent<std::string> smon{"start "}; // инициализацияstd::vector<std::future<void>> v;

for (auto i = 0; i < 5; i++) { // выполнить асинхр. задачи... v.emplace_back(std::async(std::launch::async, [&, i]{

smon([=](auto &s){ // "объявление" функции s += "i = " + std::to_string(i); s += " "; });

smon([](auto &s){ // "объявление" функции std::cout << s << std::endl; }); }));}

for (auto &f: v) // дождаться завершения f.wait();

std::cout << "done\n";167

Потокобезопасная обёртка над данными на основе очереди задач

start i = 0 start i = 0 i = 2 start i = 0 i = 2 i = 3 start i = 0 i = 2 i = 3 i = 1 start i = 0 i = 2 i = 3 i = 1 i = 4 done

start i = 0 donestart i = 0 i = 2 start i = 0 i = 2 i = 1 start i = 0 i = 2 i = 1 i = 3 start i = 0 i = 2 i = 1 i = 3 i = 4

start i = 0 start i = 0 i = 1 start i = 0 i = 1 i = 4 start i = 0 i = 1 i = 4 i = 3 start i = 0 i = 1 i = 4 i = 3 i = 2 done 168

Потокобезопасная обёртка над данными на основе очереди задач

concurrent<std::string> smon{"start "}; // инициализацияstd::vector<std::future<void>> v;

for (auto i = 0; i < 5; i++) { // выполнить асинхр. задачи... v.emplace_back(std::async(std::launch::deferred, [&, i]{

smon([=](auto &s){ // "объявление" функции s += "i = " + std::to_string(i); s += " "; });

smon([](auto &s){ // "объявление" функции std::cout << s << std::endl; }); }));}

for (auto &f: v) // дождаться завершения f.wait();

std::cout << "done\n";169

start i = 0 start i = 0 i = 1 start i = 0 i = 1 i = 2 start i = 0 i = 1 i = 2 i = 3 start i = 0 i = 1 i = 2 i = 3 i = 4 done

Потокобезопасная обёртка над данными на основе очереди задач

concurrent<std::ostream&> mon_cout{std::cout};std::vector<std::future<void>> v;

for (auto i = 0; i < 5; i++) { v.emplace_back(std::async(std::launch::async, [&, i]{ mon_cout([=](auto &cout){ cout << "i = " << std::to_string(i); cout << "\n"; }); mon_cout([=](auto &cout){ cout << "hi from " << i << std::endl; }); }));}

for (auto &f: v) f.wait();

mon_cout([](auto &cout){ cout << "done\n";}); 170

Потокобезопасная обёртка над данными на основе очереди задач

i = 0hi from 0i = 2hi from 2i = 3hi from 3i = 4hi from 4i = 1hi from 1done

i = 0hi from 0i = 2hi from 2i = 3hi from 3i = 4hi from 4i = 1hi from 1done

i = 0i = 1hi from 1hi from 0i = 2hi from 2i = 4hi from 4i = 3hi from 3done

i = 0hi from 0i = 2hi from 2i = 1i = 3hi from 3hi from 1i = 4hi from 4done

171

Потокобезопасная обёртка над данными на основе очереди задач

template<typename F>auto operator()(F f) -> std::future<decltype(f(t))> {

// создаём объект promise (shared_ptr<promise>) auto p = std::make_shared<std::promise<decltype(f(t))>>();

// получаем из promise объект future auto ret = p->get_future();

q.push([=]{ // выполняем обещание уже внутри потока try { p->set_value(f(t)); } catch (...) { p->set_exception(std::current_exception()); } });

return ret;}

Данная версия operator() позволяет вернуть значение при вызове функции:

172

Потокобезопасная обёртка над данными на основе очереди задач

template<typename F>auto operator()(F f) -> std::future<decltype(f(t))> { auto p = std::make_shared<std::promise<decltype(f(t))>>(); auto ret = p->get_future();

q.push([=]{ try { set_value(*p, f, t); } catch (...) { p->set_exception(std::current_exception()); } });

return ret;}

template<typename Fut, typename F, typename T1>void set_value(std::promise<Fut>& p, F& f, T1& t){ p.set_value(f(t)); }

template<typename F, typename T1>void set_value(std::promise<void>& p, F& f, T1& t){ f(t); p.set_value(); }

173

Потокобезопасная обёртка над данными на основе очереди задач

template<typename F>auto operator()(F f) -> std::future<decltype(f(t))> { auto p = std::make_shared<std::promise<decltype(f(t))>>(); auto ret = p->get_future();

q.push([=]{ try { set_value(*p, f, t); } catch (...) { p->set_exception(std::current_exception()); } });

return ret;}

auto f = smon([](auto &s){ s += "done\n"; std::cout << s; return s;});

std::cout << "return value: " << f.get() << std::endl; 174

Мьютексы и очереди задач

Блокировка мьютекса

▪ потоки блокируются

▪ имеется возможность дедлока

▪ небольшая масштабируемость

▪ порядок следования сообщения в логе отличается от последовательности поступления

Очереди задач

▪ потоки не блокируются

▪ отсутствует возможность дедлока

▪ высокая масштабируемость

▪ порядок следования сообщения в логе совпадает с фактическим

175

Потокобезопасная обёртка на основе очереди задач - применение

class backgrounder {public: future<bool> save(std::string file) { c([=](data& d) { ... // каждая функция - в отдельной транзакции }); }

future<size_t> print(some& stuff) { c([=, &stuff](data& d) { ... // атомарный неделимый вывод }); }

private: struct data { /* ... */ } // данные concurrent<data> c; // обёртка для потокобезопасного}; // выполнения операций с данными

176