View
916
Download
2
Category
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