ПВТ - весна 2015 - Лекция 5. Многопоточное...
TRANSCRIPT
Лекция 5. Многопоточное программирование в языке С++. Работа с потоками. Защита данных. Синхронизация. Будущие результаты
Пазников Алексей АлександровичКафедра вычислительных систем СибГУТИ
Сайт курса: http://cpct.sibsutis.ru/~apaznikov/teaching/Q/A: https://piazza.com/sibsutis.ru/spring2015/pct2015spring
Параллельные вычислительные технологииВесна 2015 (Parallel Computing Technologies, PCT 15)
О дивный новый [параллельный] мир!
#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
Мьютексы в С++
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
Будущие результаты (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
Упакованные задачи
▪ Шаблон 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
Проблемы с параллелизмом на основе потоков
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.
Но это редко. В большинстве случаев смело разрабатывайте программы на основе задач, а не на основе потоков.
Быстрая сортировка в духе функционального прог-я
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
Помните про политику запуска задачи
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
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;}
“Обещанные” результаты (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
Деструктор объекта 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;};
Неблокирующие будущие результаты (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)
$ 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
Возможные варианты:
Мьютексы и очереди задач
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