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

Preview:

DESCRIPTION

ЛЕКЦИЯ 5. Многопоточное программирование в языке С++. Работа с потоками. Защита данных. Синхронизация. Будущие результаты Курс "Параллельные вычислительные технологии" (ПВТ), осень 2014 Сибирский государственный университет телекоммуникаций и информатики преподаватель: Пазников Алексей Александрович к.т.н., доцент кафедры вычислительных систем СибГУТИ

Citation preview

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

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

Сайт курса: http://cpct.sibsutis.ru/~apaznikov/teaching/Вопросы: https://piazza.com/sibsutis.ru/fall2014/pct14/home

Параллельные вычислительные технологииОсень 2014 (Parallel Computing Technologies, PCT 14)

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

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

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

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

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

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

21

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

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

CPU CPU CPU

accumulate_block accumulate_block accumulate_block

22

Параллельная версия алгоритма 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); }};

23

Параллельная версия алгоритма 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);

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

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

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

24

Параллельная версия алгоритма 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); }

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

25

Параллельная версия алгоритма 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;}

26

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

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

28

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

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

29

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

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

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

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

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

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

};

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

};

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

stack<int> s;

if (!s.empty()) {

int const value = s.top();

s.pop();

// ...

}

34

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

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

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

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

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

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

std::shared_ptr<T> pop()

35

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

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 отсутствует // -- интерфейс предельно упрощён --};

36

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

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

37

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

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 - “всё или ничего”

38

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

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

1 -> 2 -> 3

1

2

3

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

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

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

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

1

2

3

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

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

40

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

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(); // вызов недопустим} 41

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

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

42

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

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

43

Блокировка с помощью 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;}

44

Блокировка с помощью 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;}

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

45

Блокировка с помощью 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); // вывести результат}

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

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

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

46

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

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() { /* ... */ }} А если несколько

потоков?47

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

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

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

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) { if (connection_flag == false) { // гонка! std::lock_guard<std::mutex> lock(mut); if (connection_flag == false) { connection = open_connection(); connection_flag = true; // гонка! } }

connection.send(data); }

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

49

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

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

50

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

51

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

52

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

▪ std::recursive_mutex

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

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

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

53

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

▪ std::condition_variable, std::condition_variable_any

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

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

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

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

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

54

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

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

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

56

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

std::future<> & std::shared_future<>57

Будущие результаты (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

58

Будущие результаты (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);

59

Будущие результаты (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);

60

Будущие результаты (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; // только ожидание // результата

61

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

task 1 task 2 task 3

package 1 package 2 package 3

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)

63

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

task

package

task

package

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

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

batch_systemadd_task

64

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

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(); // запуск задачи} } 65

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

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

66

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

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

67

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

$ ./prog voxpopuli

,

$ ./prog voxpopuli

,voxdei

$ ./prog voxpopuli

,vox

68

“Обещанные” результаты (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();} 69

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

mainmain thread

th1print_value

prom.set_value()

print_value

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

fut.get()

compute_value

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

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

70

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

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

71

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

▪ “Код с запашком” (code smell)Например, потоки выполняют код, который не нуждается в блокировке мьютекса (например, один поток инициализирует структуру, после чего сообщает другому, что структура готова).

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

▪ Ложное пробуждение (spurious wakeup)Поток, ожидающий сигнала может проснуться тогда, когда сигнал не был отправлен (или когда он был отправлен не этому потоку, или когда условие перестало выполняться). Нужна дополнительная проверка! (return !widget_queue.empty();) А что, если он не может проверить?! 72

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

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

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

73

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

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

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

ok, let’s move!

p.set_value()

74

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

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

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

ok, let’s move!

p.set_value()

Можно отправить сигнал только один раз

75

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

std::promise<void> p;

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

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

76

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

std::promise<void> p;

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

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

77

Множественная отправка сигналов или пример 1 2/3

std::promise<void> p;

void detect() {

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

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

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

// ...

p.set_value();

// ...

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

78

“Обещанные” результаты (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(); }); 79

“Обещанные” результаты (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();}

80

“Обещанные” результаты (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()

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

81

“Обещанные” результаты (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

82

Разделяемые будущие результаты 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; }; 83

Разделяемые будущие результаты 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";}

84

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

main

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

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

T1

T2t2_ready.set_value

start

return

return

output

ready.set_value

t1_ready.set_value

85

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

86

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

template<typename T>std::list<T> sequential_quick_sort(std::list<T> input) { if (input.empty()) { return 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; } 87

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

lowerpart

input

input

pivot

splice

divide_point

new_lower

new_higher

88

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

template<typename T>std::list<T> parallel_quick_sort(std::list<T> input) { if (input.empty()) { return 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; } 89

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

90

Неблокирующие будущие результаты (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"; }91

Неблокирующие будущие результаты (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();

92

Неблокирующие будущие результаты (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() для ожидания результата.

93

Неблокирующие будущие результаты (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

94

Неблокирующие будущие результаты (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(); 95

Неблокирующие будущие результаты (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();

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

Неблокирующие будущие результаты (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();

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

97

Неблокирующие будущие результаты (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

98

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

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

f2

f3

f1

f

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

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

▪ поток один

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

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

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

Ожидание выполнения всех задач (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();

100

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

f2

f1f3

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

f4

101

Ожидание выполнения всех задач (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>>>

102

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

f2

f1f3

f4

f5

103

Ожидание выполнения всех задач (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

104

Ожидание выполнения какой-либо задачи (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;105

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

f2

f1f3

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

f4

106

Ожидание выполнения какой-либо задачи (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

107

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

108

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

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;

}

};

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

109

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

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;

});

}

};

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

110

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

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

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

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

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

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

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

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

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

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

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

111

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

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

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

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

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

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

112

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

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

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

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

114

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

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";115

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

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 116

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

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

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

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

118

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

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

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

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";120

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

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 121

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

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

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

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

123

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

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() позволяет вернуть значение при вызове функции:

124

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

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

125

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

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

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

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; // обёртка для потокобезопасного}; // выполнения операций с данными

127

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

128

Цель разработки параллельных структур данных

▪ Обеспечить параллельный доступ ▪ Обеспечить безопасность доступа▪ Минимизировать взаимные исключения▪ Минимизировать сериализацию

129

Цель разработки параллельных структур данных

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

инварианты нарушены▪ Предотвратить состояние гонки▪ Предусмотреть возникновение исключений▪ Минимизировать возможность взаимоблокировок

Средства достижения:▪ ограничить область действия блокировок▪ защитить разные части структуры разными

мьютексами▪ обеспечить разный уровень защиты▪ изменить структуру данных для расширения

возможностей распраллеливания 130

Потокобезопасный стек - потенциальные проблемы

Потенциальные проблемы безопасности реализации потокобезопасных структур:

1. Гонки данных

2. Взаимоблокировки

3. Безопасность относительно исключений

4. ...

131

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

struct empty_stack: std::exception { };

template<typename T> class threadsafe_stack {private: std::stack<T> data; mutable std::mutex m;

public: threadsafe_stack() {} threadsafe_stack(const threadsafe_stack &other) { std::lock_guard<std::mutex> lock(other.m); data = other.data; }

threadsafe_stack &operator=(const threadsafe_stack&) = delete;

void push(T new_value) { std::lock_guard<std::mutex> lock(m); data.push(std::move(new_value)); }

Защита данных

132

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

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

bool empty() const { std::lock_guard<std::mutex> lock(m); return data.empty(); }};

133

Потокобезопасный стек - тестовая программа

threadsafe_stack<int> stack;

void pusher(unsigned nelems) { for (unsigned i = 0; i < nelems; i++) { stack.push(i); }}

void printer() { try { for (;;) { int val; stack.pop(val); } } catch (empty_stack) { std::cout << "stack is empty!" << std::endl; }}

int main() { std::thread t1(pusher, 5), t2(pusher, 5); t1.join(); t2.join(); std::thread t3(printer); t3.join();} 134

Потокобезопасный стек - безопасность исключений

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

data.pop(); return value; }

[невозвратная] модификация контейнера

2

1

3

4

135

Версия pop, безопасная с точки зрения исключений

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>(std::move(data.top())));

data.pop(); return res; }

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

data.pop(); }

1

2

3

4

5

6

[невозвратная] модификация контейнера

[невозвратная] модификация контейнера

136

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

struct empty_stack: std::exception { };

template<typename T> class threadsafe_stack {private: std::stack<T> data; mutable std::mutex m;

public: threadsafe_stack() {} threadsafe_stack(const threadsafe_stack &other) { std::lock_guard<std::mutex> lock(other.m); data = other.data; }

threadsafe_stack &operator=(const threadsafe_stack&) = delete;

void push(T new_value) { std::lock_guard<std::mutex> lock(m); data.push(std::move(new_value)); }

DEADLOCK ?137

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

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>(std::move(data.top()))); data.pop(); return res; }

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

DEADLOCK ?

DEADLOCK ?

138

threadsafe_stack<int> stack;

void pusher(unsigned nelems) { for (unsigned i = 0; i < nelems; i++) { stack.push(i); }}

void printer() { try { for (;;) { int val; stack.pop(val); } } catch (empty_stack) { std::cout << "stack is empty!" << std::endl; }}

int main() { std::thread t1(pusher, 5), t2(pusher, 5); t1.join(); t2.join(); std::thread t3(printer); t3.join();}

Потокобезопасный стек - тестовая программа

Недостатки реализации:

▪ Сериализация потоков приводит к снижению производительности: потоки простаивают и не совершают полезной работы

▪ Нет средств, позволяющих ожидать добавления элемента

139

template<typename T> class threadsafe_queue {private: mutable std::mutex mut; std::queue<T> data_queue; std::condition_variable data_cond;

public: threadsafe_queue() {}

void push(T new_value) { std::lock_guard<std::mutex> lk(mut); data_queue.push(std::move(new_value)); data_cond.notify_one(); } std::shared_ptr<T> wait_and_pop() { std::unique_lock<std::mutex> lk(mut); data_cond.wait(lk, [this]{return !data_queue.empty();}); std::shared_ptr<T> res( std::make_shared<T>(std::move(data_queue.front()))); data_queue.pop(); return res; }

Потокобезопасная очередь с ожиданием

140

Потокобезопасная очередь с ожиданием

void wait_and_pop(T &value) { std::unique_lock<std::mutex> lk(mut); data_cond.wait(lk, [this]{return !data_queue.empty();}); value = std::move(data_queue.front()); data_queue.pop(); }

bool try_pop(T& value) { std::lock_guard<std::mutex> lk(mut); if (data_queue.empty()) return false; value = std::move(data_queue.front()); data_queue.pop(); return true; }

std::shared_ptr<T> try_pop() { // ... }

bool empty() const { /* ... */ }

141

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

threadsafe_queue<int> queue;

void pusher(unsigned nelems) { for (unsigned i = 0; i < nelems; i++) { queue.push(i); }}

void poper(unsigned nelems) { for (unsigned i = 0; i < nelems; i++) { int val; queue.wait_and_pop(val); }}

int main() { std::thread t1(pusher, 5), t2(pusher, 5), t3(poper, 9);

t1.join(); t2.join(); t3.join();}

Не требуется проверка empty()

142

Потокобезопасная очередь с ожиданием

void wait_and_pop(T &value) { std::unique_lock<std::mutex> lk(mut); data_cond.wait(lk, [this]{return !data_queue.empty();}); value = std::move(data_queue.front()); data_queue.pop(); }

bool try_pop(T& value) { std::lock_guard<std::mutex> lk(mut); if (data_queue.empty()) return false; value = std::move(data_queue.front()); data_queue.pop(); return true; }

std::shared_ptr<T> try_pop() { // ... }

bool empty() const { /* ... */ }

Не вызывается исключение

143

template<typename T> class threadsafe_queue {private: mutable std::mutex mut; std::queue<T> data_queue; std::condition_variable data_cond;public: threadsafe_queue() {}

void push(T new_value) { std::lock_guard<std::mutex> lk(mut); data_queue.push(std::move(new_value)); data_cond.notify_one(); } std::shared_ptr<T> wait_and_pop() { std::unique_lock<std::mutex> lk(mut); data_cond.wait(lk, [this]{return !data_queue.empty();}); std::shared_ptr<T> res( std::make_shared<T>(std::move(data_queue.front()))); data_queue.pop(); return res; }

Очередь с ожиданием - безопасность исключений

При срабатывании исключения

в wait_and_pop (в ходе инициализации res)

другие потоки не будут разбужены

144

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

template<typename T> class threadsafe_queue {private: mutable std::mutex mut; std::queue<std::shared_ptr<T>> data_queue; std::condition_variable data_cond;

public: void push(T new_value) { std::shared_ptr<T> data( std::make_shared<T>(std::move(new_value))); std::lock_guard<std::mutex> lk(mut); data_queue.push(std::move(new_value)); data_cond.notify_one(); } std::shared_ptr<T> wait_and_pop() { std::unique_lock<std::mutex> lk(mut); data_cond.wait(lk, [this]{ return !data_queue.empty(); }); std::shared_ptr<T> res = data_queue.front(); data_queue.pop(); return res; }

Очередь теперь хранит элементы

shared_ptr

Инициализация объекта теперь

выполняется не под защитой блокировки (и это весьма хорошо)

Объект извлекается напрямую 145

void wait_and_pop(T &value) { std::unique_lock<std::mutex> lk(mut); data_cond.wait(lk, [this]{return !data_queue.empty();}); value = std::move(*data_queue.front()); data_queue.pop(); }

bool try_pop(T& value) { std::lock_guard<std::mutex> lk(mut); if (data_queue.empty()) return false; value = std::move(*data_queue.front()); data_queue.pop(); return true; }

std::shared_ptr<T> try_pop() { // ... }

bool empty() const { /* ... */ }

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

Объект извлекается из

очереди напрямую, shared_ptr не

инициализируется- исключение не возбуждается!

146

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

std::shared_ptr<T> wait_and_pop() { std::unique_lock<std::mutex> lk(mut); data_cond.wait(lk, [this]{ return !data_queue.empty(); }); std::shared_ptr<T> res = data_queue.front(); data_queue.pop(); return res; }

bool try_pop(T& value) { std::lock_guard<std::mutex> lk(mut); if (data_queue.empty()) return false; value = std::move(*data_queue.front()); data_queue.pop(); return true; }

std::shared_ptr<T> try_pop() { // ... }

bool empty() const { /* ... */ }

Объект извлекается из очереди напрямую,

shared_ptr не инициализируется

Недостатки реализации:

▪ Сериализация потоков приводит к снижению производительности: потоки простаивают и не совершают полезной работы

147

Очередь с мелкозернистыми блокировками

Head Tail

148

Очередь с мелкозернистыми блокировками

template<typename T> class queue {private: struct node { T data; std::unique_ptr<node> next; node(T _data): data(std::move(_data)) {} }; std::unique_ptr<node> head; node* tail;

public: queue() {} queue(const queue &other) = delete; queue& operator=(const queue &other) = delete;

Использование unique_ptr<node>

гарантирует удаление узлов без

использования delete

149

Очередь с мелкозернистыми блокировками

std::shared_ptr<T> try_pop() { if (!head) { return std::shared_ptr<T>(); } std::shared_ptr<T> const res( std::make_shared<T>(std::move(head->data))); std::unique_ptr<node> const old_head = std::move(head); head = std::move(old_head->next); return res; }

void push(T new_value) { std::unique_ptr<node> p(new node(std::move(new_value))); node* const new_tail = p.get(); if (tail) tail->next = std::move(p); else head = std::move(p); tail = new_tail; } }; 150

Очередь с мелкозернистыми блокировками

std::shared_ptr<T> try_pop() { if (!head) { return std::shared_ptr<T>(); } std::shared_ptr<T> const res( std::make_shared<T>(std::move(head->data))); std::unique_ptr<node> const old_head = std::move(head); head = std::move(old_head->next); return res; }

void push(T new_value) { std::unique_ptr<node> p(new node(std::move(new_value))); node* const new_tail = p.get(); if (tail) tail->next = std::move(p); else head = std::move(p); tail = new_tail; } };

push изменяет как tail, так и

head

необходимо будет защищать оба одновременно 151

Очередь с мелкозернистыми блокировками

std::shared_ptr<T> try_pop() { if (!head) { return std::shared_ptr<T>(); } std::shared_ptr<T> const res( std::make_shared<T>(std::move(head->data))); std::unique_ptr<node> const old_head = std::move(head); head = std::move(old_head->next); return res; }

void push(T new_value) { std::unique_ptr<node> p(new node(std::move(new_value))); node* const new_tail = p.get(); if (tail) tail->next = std::move(p); else head = std::move(p); tail = new_tail; } };

pop и push обращаются к head->next и tail->next

если в очереди 1 элемент, то head->next и tail->next -

один и тот же объект 152

Очередь с мелкозернистыми блокировками

Head Tail

next next

153

Модифицированная версия

Head Tail

Фиктивный узел

▪ При пустой очереди head и tail указывают на фиктивный узел, а не равны NULL, причём head == tail.

▪ При очереди с одним элементом head->next и tail->next указывают на разные узлы (причём head->next == tail), в результате чего гонки не возникает. 154

Пустая очередь

Head Tail

Фиктивный узел

▪ При пустой очереди head и tail указывают на фиктивный узел, а не равны NULL, причём head == tail.

155

Очередь с одним элементом

Head Tail

Фиктивный узел

▪ При пустой очереди head и tail указывают на фиктивный узел, а не равны NULL, причём head == tail.

▪ При очереди с одним элементом head->next и tail->next указывают на разные узлы (причём head->next == tail), в результате чего гонки не возникает. 156

Очередь с мелкозернистыми блокировками

template<typename T>class queue {private: struct node { std::shared_ptr<T> data; std::unique_ptr<node> next; }; std::unique_ptr<node> head; node *tail;

public: queue(): head(new node), tail(head.get()) {} queue(const queue &other) = delete; queue &operator=(const queue &other) = delete;

node хранит указатель на данные

▪ Вводится фиктивный узел▪ При пустой очереди head и tail теперь

указывают на фиктивный узел, а не на NULL

указатель на данные вместо данных

создание первого фиктивного узла в конструкторе

157

Очередь с мелкозернистыми блокировками

std::shared_ptr<T> try_pop() { if (head.get() == tail) { return std::shared_ptr<T>(); } std::shared_ptr<T> const res(head->data); std::unique_ptr<node> old_head = std::move(head); head = std::move(old_head->next); return res; }

void push(T new_value) { std::shared_ptr<T> new_data( std::make_shared<T>(std::move(new_value))); std::unique_ptr<node> p(new node); tail->data = new_data; node *const new_tail = p.get(); tail->next = std::move(p); tail = new_tail; }

head сравнивается с tail, а не с NULL

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

создание нового экземпляра T

создание нового фиктивного узла

записываем в старый фиктивный узел новое

значение 158

Добавление нового элемента в очередь (push)

tail next

data

159

Добавление нового элемента в очередь (push)

tail next

data

p(new node)

160

Добавление нового элемента в очередь (push)

tail next

data

tail->data = new_data

p(new node)

161

Добавление нового элемента в очередь (push)

tail next

data

new_tail

new_tail = p.get()

next

data

p(new node)

162

Добавление нового элемента в очередь (push)

tail next new_tail

tail->next = std::move(p)

data

next

data

163

Добавление нового элемента в очередь (push)

tail next

data

tail = new_tail

164

Очередь с мелкозернистыми блокировками

std::shared_ptr<T> try_pop() { if (head.get() == tail) { return std::shared_ptr<T>(); } std::shared_ptr<T> const res(head->data); std::unique_ptr<node> old_head = std::move(head); head = std::move(old_head->next); return res; }

void push(T new_value) { std::shared_ptr<T> new_data( std::make_shared<T>(std::move(new_value))); std::unique_ptr<node> p(new node); tail->data = new_data; node *const new_tail = p.get(); tail->next = std::move(p); tail = new_tail; }

обращение к tail только на момент

начального сравнения

push обращается

только к tail

try_pop обращается

только к head

165

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

Head Tail

▪ Функция push обращается только к tail, try_pop - только к head (и tail на короткое время).

▪ Вместо единого глобального мьютекса можно завести два отдельных и удерживать блокировки при доступке к head и tail.

1 2

166

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

template<typename T> class queue {private: struct node { std::shared_ptr<T> data; std::unique_ptr<node> next; };

std::mutex head_mutex, tail_mutex; std::unique_ptr<node> head; node *tail;

node *get_tail() { std::lock_guard<std::mutex> tail_lock(tail_mutex); return tail; }

std::unique_ptr<node> pop_head() { std::lock_guard<std::mutex> head_lock(head_mutex); if (head.get() == get_tail()) return nullptr std::unique_ptr<node> old_head = std::move(head); head = std::move(old_head->next); return old_head; }

блокируется только на момент получения элемента tail

167

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

public: threadsafe_queue(): head(new node), tail(head.get()) {} threadsafe_queue(const threadsafe_queue &other) = delete; threadsafe_queue &operator=(const threadsafe_queue &other)=delete;

std::shared_ptr<T> try_pop() { std::unique_ptr<node> old_head = pop_head(); return old_head ? old_head->data : std::shared_ptr<T>(); }

void push(T new_value) { std::shared_ptr<T> new_data( std::make_shared<T>(std::move(new_value))); std::unique_ptr<node> p(new node); node* const new_tail = p.get(); std::lock_guard<std::mutex> tail_lock(tail_mutex); tail->data = new_data; tail->next = std::move(p); tail = new_tail; }};

push обращается только к tail, но не к head, поэтому используется одна блокировка

168

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

public: threadsafe_queue(): head(new node), tail(head.get()) {} threadsafe_queue(const threadsafe_queue &other) = delete; threadsafe_queue &operator=(const threadsafe_queue &other)=delete;

std::shared_ptr<T> try_pop() { std::unique_ptr<node> old_head = pop_head(); return old_head ? old_head->data : std::shared_ptr<T>(); }

void push(T new_value) { node *const old_tail = get_tail(); std::lock_guard<std::mutex> head_lock(head_mutex); if (head.get() == old_tail) { return nullptr; } std::unique_ptr<node> old_head = std::move(head); head = std::move(old_head->next); return old_head; }};

выполняется не под защитой мьютекса

head_mutex

169

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

public: threadsafe_queue(): head(new node), tail(head.get()) {} threadsafe_queue(const threadsafe_queue &other) = delete; threadsafe_queue &operator=(const threadsafe_queue &other)=delete;

std::shared_ptr<T> try_pop() { std::unique_ptr<node> old_head = pop_head(); return old_head ? old_head->data : std::shared_ptr<T>(); }

void push(T new_value) { node *const old_tail = get_tail(); std::lock_guard<std::mutex> head_lock(head_mutex); if (head.get() == old_tail) { return nullptr; } std::unique_ptr<node> old_head = std::move(head); head = std::move(old_head->next); return old_head; }};

выполняется не под защитой мьютекса

head_mutex

170

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

Особенности реализации:

▪ Освободить мьютекс в push до вызова notify_one, чтобы разбуженный поток не ждал освобождения мьютекса.

▪ Проверку условия можно выполнять под защитой head_mutex, захватывая tail_mutex только для чтения tail. Предикат выглядит как head != get_tail()

▪ Для версии pop, работающей со ссылкой, необходимо переопределить wait_and_pop(), чтобы обеспечить безопасность с точки зрения исключений. Необходимо сначала скопировать данные из узла, а потом удалять узел из списка.

171

Потокобезопасная очередь с мелкозернистыми блокировками и с ожиданием поступления элемента - объявление класса

template<typename T> class queue {private: struct node { std::shared_ptr<T> data; std::uniquet_ptr<node> next; }; std::mutex head_mutex, tail_mutex; std::unique_ptr<node> head; node *tail; std::condition_variable data_cond;

public: threadsafe_queue(): head(new node), tail(head.get()) {} threadsafe_queue(const threadsafe_queue& other) = delete; std::shared_ptr<T> try_pop(); bool try_pop(T& value); std::shared_ptr<T> wait_and_pop(); void wait_and_pop(T& value); void push(T new_value); void empty(); };

172

Потокобезопасная очередь с мелкозернистыми блокировками и с ожиданием поступления элемента - добавление новых значений

template<typename T>void threadsafe_queue<T>::push(T new_value) { std::shared_ptr<T> new_data( std::make_shared<T>(std::move(new_value)));

std::unique_ptr<node> p(new node); { std::lock_guard<std::mutex> tail_lock(tail_mutex); tail->data = new_data; node* const new_tail = p.get(); tail->next = std::move(p); tail = new_tail; }

data_cond.notify_one();}

173

Потокобезопасная очередь с мелкозернистыми блокировками и с ожиданием поступления элемента - ожидение и извлечение элемента

template<typename T> class threadsafe_queue {

private: node* get_tail() { std::lock_guard<std::mutex> tail_lock(tail_mutex); return tail; }

std::unique_ptr<node> pop_head() { std::unique_ptr<node> old_head = std::move(head); head = std::move(old_head->next); return old_head; }

std::unique_lock<std::mutex> wait_for_data() { std::unique_lock<std::mutex> head_lock(head_mutex); data_cond.wait(head_lock, [&]{return head.get() != get_tail(); }); return std::move(head_lock); }

Модификация списка в результате удаления головного элемента.

Ожидание появления данных в очередиВозврат объекта блокировки 174

Потокобезопасная очередь с мелкозернистыми блокировками и с ожиданием поступления элемента - ожидение и извлечение элемента

std::unique_ptr<node> wait_pop_head() { std::unique_lock<std::mutex> head_lock(wait_for_data()); return pop_head(); }

std::unique_ptr<node> wait_pop_head(T& value) { std::unique_lock<std::mutex> head_lock(wait_for_data()); value = std::move(*head->data); return pop_head(); }

public: std::shared_ptr<T> wait_and_pop() { std::unique_ptr<node> const old_head = wait_pop_head(); return old_head->data; }

void wait_and_pop(T& value) { std::unique_ptr<node> const old_head = wait_pop_head(value); }};

Модификация данных под защитой мьютекса, захваченного в wait_for_data

Модификация данных под защитой мьютекса, захваченного в wait_for_data

175

Потокобезопасная очередь с мелкозернистыми блокировками и с ожиданием поступления элемента - try_pop() и empty()

private:

std::unique_ptr<node> try_pop_head() { std::lock_guard<std::mutex> head_lock(head_mutex); if (head.get() == get_tail()) { return std::unique_ptr<node>(); } return pop_head(); }

std::unique_ptr<node> try_pop_head(T& value) { std::lock_guard<std::mutex> head_lock(head_mutex); if (head.get() == get_tail()) { return std::unique_ptr<node>(); } value = std::move(*head->data); return pop_head(); }

176

Потокобезопасная очередь с мелкозернистыми блокировками и с ожиданием поступления элемента - try_pop() и empty()

public:

std::shared_ptr<T> try_pop() { std::unique_ptr<node> old_head = try_pop_head(); return old_head ? old_head->data : std::shared_ptr<T>(); }

bool try_pop(T& value) { std::unique_ptr<node> const old_head = try_pop_head(value); return old_head; }

void empty() { std::lock_guard<std::mutex> head_lock(head_mutex); return (head.get() == get_tail()); }};

177

Recommended