ЛЕКЦИЯ 5. Многопоточное программирование в языке С++. Работа с потоками. Защита данных. Синхронизация. Будущие результаты
Курс "Параллельные вычислительные технологии" (ПВТ), осень 2014
Сибирский государственный университет телекоммуникаций и информатики
преподаватель:
Пазников Алексей Александрович
к.т.н., доцент кафедры вычислительных систем СибГУТИ
Similar to ПВТ - осень 2014 - Лекция 5 - Многопоточное программирование в языке С++. Работа с потоками. Защита данных. Синхронизация. Будущие результаты
Борис Сазонов, RAII потоки и CancellationToken в C++Sergey Platonov
Similar to ПВТ - осень 2014 - Лекция 5 - Многопоточное программирование в языке С++. Работа с потоками. Защита данных. Синхронизация. Будущие результаты (20)
10. ˘˱̀̄̂˻˾˶̂˻˿˼̍˻˹̆̀˿̃˿˻˿˳˹˿˷˹˵˱˾˹˶˸˱˳˶́̉˶˾˹̐
int main() {
std::vectorstd::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
11. ˘˱̀̄̂˻˾˶̂˻˿˼̍˻˹̆̀˿̃˿˻˿˳˹˹˵˶˾̃˹̅˹˻˱̃˿́̌̀˿̃˿˻˿˳
std::vectorstd::thread threads;
std::mapstd::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
31. ˝̍̏̃˶˻̂̌˳ˢ
class wrapper {
private:
data_t data; // защищаемые данные
std::mutex mut;
public:
templatetypename Function
void proc_data(Function func) {
std::lock_guardstd::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
32. ˝̍̏̃˶˻̂̌˳ˢ
class wrapper {
private:
data_t data; // защищаемые данные
std::mutex mut;
public:
templatetypename Function
void proc_data(Function func) {
Нельзя передавать указатели и
ссылки на защищённые данные
за пределы области видимости
блокировки никаким образом.
std::lock_guardstd::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
35. ˑ˵˱̀̃˱̇˹̐˹˾̃˶́̅˶˺̂˿˳˻˽˾˿˴˿̀˿̃˿̈˾˿̂̃˹
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);
};
stackint s;
if (!s.empty()) {
int const value = s.top();
s.pop();
// ...
}
некорректный
результат как решить?
34
36. ˑ˵˱̀̃˱̇˹̐˹˾̃˶́̅˶˺̂˿˳˻˽˾˿˴˿̀˿̃˿̈˾˿̂̃˹
1. Передавать ссылку в функцию pop
std::vectorint result;
mystack.pop(result);
2. Потребовать наличия копирующего или перемещающего
конструктора, не возбуждающего исключений (доказано, что
можно объединить pop и top, но это можно сделать только если
конструкторы не вызывают исключений)
3. Возвращать указатель на вытолкнутый элемент
std::shared_ptrT pop()
4. Одновременно 1 и один из вариантов 2 или 3
35
45. ˒˼˿˻˹́˿˳˻˱̂̀˿˽˿̊̍̏VWGXQLTXHBORFN
class Widget {
int val;
std::mutex m;
int getval() const {
return val;
}
};
bool Cmp(Widget lhs, Widget rhs) {
// не захватываем пока мьютексы
std::unique_lockstd::mutex lock1(lhs.m,std::defer_lock);
std::unique_lockstd::mutex lock2(rhs.m,std::defer_lock);
// а вот сейчас захватываем, причём без дедлоков
std::lock(lock1, lock2);
return lhs.getval() rhs.getval() ? true : false;
}
44
46. ˒˼˿˻˹́˿˳˻˱̂̀˿˽˿̊̍̏VWGXQLTXHBORFN
class Widget {
int val;
std::mutex m;
int getval() const {
std::lock_guardstd::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
47. ˒˼˿˻˹́˿˳˻˱̂̀˿˽˿̊̍̏VWGXQLTXHBORFN
void pop_and_process() {
std::unique_lockstd::mutex lock(mut);
Widget data = queue.pop(); // получить элемент данных
lock.unlock(); // освободить мьютекс
super_widget result = process(data); // обработать данные
lock.lock(); // опять захватить мьютекс
output_result(data, result); // вывести результат
}
Минимизация блокировок!
▪ блокировать данные, а не операции
▪ удерживать мьютекс столько, сколько необходимо
▫ тяжёлые операции (захват другого мьютекса,
ввод/вывод и т.д.) - вне текущей критической секции
46
52. 5:˽̍̏̃˶˻̂̌˳ˢ
class Widget {
mutable std::shared_timed_mutex mut;
int data;
public:
Widget operator=(const R rhs) {
// эксклюзивные права на запись в *this
std::unique_lockstd::shared_timed_mutex
lhs(mut, std::defer_lock);
// разделяемые права на чтение rhs
std::shared_lockstd::shared_timed_mutex
rhs(other.mut, std::defer_lock);
std::lock(lhs, rhs);
// выполнить присваивание
data = rhs.data;
return *this;
}
};
51
54. ˡ˶˻̄́̂˹˳˾̌˶˽̍̏̃˶˻̂̌
▪ std::recursive_mutex
▪ мьютекс можно запирать несколько раз в
одном потоке
▪ освобождать мьютекс требуется столько
раз, сколько он был захвачен
▪ использование - аналогично std::mutex
(std::lock_guard, std::unique_lock,
…)
53
55. ˤ̂˼˿˳˾̌˶̀˶́˶˽˶˾˾̌˶
▪ std::condition_variable, std::
condition_variable_any
условная переменная, необходимо взаимодействие с
мьютексом (condition_variable) или с любым
классом (condition_variable_any), подобным
мьютексу
▪ wait - ожидание условия
▪ wait_for, wait_until - ожидание условия заданное
время или до заданного момента
▪ notify_one - сообщить одному потоку
▪ notify_all - сообщить всем потокам
54
61. int thinking();
// Запуск асинхронной (“фоновой”) задачи
std::futureint answer = std::async(thinking);
// Работа основного потока
do_other_stuff(); // в этом время работает thinking()
// Получение результатов
std::cout The answer is answer.get() std::endl;
main thread T1
answer.get()
thinking... T2
работа ожидание
async
58
63. struct Widget {
void foo(std::string const, int);
int bar(std::string const);
int operator()(int);
};
Widget w;
// Вызывается foo(carpe dieum, 2014) для объекта w
auto f1 = std::async(Widget::foo, w, carpe diem, 2014);
// Вызывается bar(carpe dieum, 2014) для объекта tmp = w
auto f2 = std::async(Widget::bar, w, carpe diem);
// Вызывается tmp.operator(2014), где tmp = w
auto f3 = std::async(Widget(), 2014);
// Вызвается w(1234)
auto f4 = std::async(std::ref(w), 2014);
59
67. ▪ 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
69. ˤ̀˱˻˿˳˱˾˾̌˶˸˱˵˱̈˹
▪ Шаблон std::packaged_task связывается будущий
результат (future) с функцией
▪ Вызов функции происходит при вызове объекта
packaged_task
▪ Параметр шаблона - сигнатура функции
template
class packaged_taskint(float, char) {
public:
templatetypename Callable
explicit packaged_task(Callable func);
std::futureint get_future();
void operator()(std::vectorchar*, int);
};
пример
спецификации
шаблона для
сигнатуры
функции
int func(float,
char)
63
77. ̀́˹˽˶́
void print_value(std::futureint 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::promiseint prom;
// Получаем объект future из созданного promise (обещаем)
std::futureint fut = prom.get_future();
// Отправляем будущее значение в новый поток
std::thread th1 (print_value, std::ref(fut));
int val = compute_value();
prom.set_value(val); // Выполняем обещание
th1.join();
} 69
81. ˞˶˵˿̂̃˱̃˻˹̂˹˾̆́̇˹˹˾˱˿̂˾˿˳˶̄̂˼˿˳˾̌̆̀˶́˶˽˶˾˾̌̆
▪ “Код с запашком” (code smell)
Например, потоки выполняют код, который не
нуждается в блокировке мьютекса (например, один
поток инициализирует структуру, после чего сообщает
другому, что структура готова).
▪ Пропущенный сигнал
Поток может отправить сигнал (notify_one/all) в тот
момент, когда другой поток не начал ожидать
▪ Ложное пробуждение (spurious wakeup)
Поток, ожидающий сигнала может проснуться тогда,
когда сигнал не был отправлен (или когда он был
отправлен не этому потоку, или когда условие перестало
выполняться). Нужна дополнительная проверка!
(return !widget_queue.empty();)
А что, если он не может проверить?! 72
89. ̀́˹˽˶́
std::promisevoid p;
void react(); // реакция на условие
void detect() { // обнаружение условия
std::thread t([] {
p.get_future().wait()
react();
});
// делаем что-то // в это время t спит
p.set_value(); // разбудить t
// делаем ещё что-то
t.join();
};
76
91. ̀́˹˽˶́
std::promisevoid p;
void react(); // реакция на условие
void detect() { // обнаружение условия
std::thread t([] {
p.get_future().wait()
react();
});
// а что, если здесь возникнет исключение??
p.set_value(); // разбудить t
// делаем ещё что-то
t.join();
};
77
92. ˝˾˿˷˶̂̃˳˶˾˾˱̐˿̃̀́˱˳˻˱̂˹˴˾˱˼˿˳˹˼˹̀́˹˽˶́
std::promisevoid p;
void detect() {
auto sf = p.get_future().share();
std::vectorstd::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
96. ̀́˹˽˶́
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
102. ˡ˱˸˵˶˼̐˶˽̌˶˲̄˵̄̊˹˶́˶˸̄˼̍̃˱̃̌VKDUHGBIXWXUH
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 startn
Thread 2 received the signal
result2.get().count() ms after startn;
}
84
110. 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
112. 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
114. 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
116. $ ./prog
begin thinking over the answer...
continue thinking over the answer...
waiting for the answer...
do some useful stuff
answer: still thinking...
number 42
do some other useful stuff
94
124. $ g++ -Wall -pedantic -pthread -lboost_system
-lboost_thread -std=c++14 -O2 prog.cpp -o prog
$ ./prog
waiting for the answer...
do some useful stuff
begin thinking over the answer...
continue thinking over the answer...
still thinking...
answer: number 42
do some other useful stuff
98
126. Блокирующие future Неблокирующие future
f2
f3
f1
f
▪ порядок выполнения
▪ устанавливается явный
неопределён
порядок выполнения
▪ возможны блокировки
▪ нет блокировок
▪ для каждой задачи
▪ поток один
создаётся отдельный поток 99
130. f2
f1
f3
f4
Будущий результат f4 зависит от выполнения всех
будущих результатов f1, f2, f3 и начинает выполняться
после завершения выполнения задач, им соответствующих
(подобно барьерной синхронизации).
101
140. f2
f1
f3
f4
Будущий результат f4 зависит от выполнения одного из
будущих результатов f1, f2, f3 и начинает выполняться
после завершения выполнения хотя бы одной задачи
(подобно синхронизации “эврика”).
106
146. ˝̍̏̃˶˻̂̌˹˿̈˶́˶˵˹˸˱˵˱̈
Блокировка мьютекса
▪ потоки блокируются
▪ имеется возможность
дедлока
▪ небольшая
масштабируемость
▪ порядок следования
сообщения в логе отличается
от последовательности
поступления
Очереди задач
▪ потоки не блокируются
▪ отсутствует возможность
дедлока
▪ высокая масштабируемость
▪ порядок следования
сообщения в логе совпадает
с фактическим
111
147. ˠ˱̃̃˶́˾̀˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿˲̒́̃˻˱˾˱˵˵˱˾˾̌˽˹
Требования к потокобезопасным “обёрткам”:
1. Сохранение интерфейса
widget w; = w.func(hi folks!);
wrapperwidget = w.func(hi folks!);
2. Универсальность. Заранее может быть неизвестны
методы, которые необходимо обернуть. Некоторые методы
сложно обернуть: конструкторы, операторы, шаблоны и т.д.
3. Поддержка транзакций
account.deposit(Sergey, 1000)
account.withdraw(Ivan, 1000);
log.print(user , username, data );
log.print(time , logmsg);
Реализация отдельных методов может не обеспечить
необходимую гранулярность.
112
148. ˠ˱̃̃˶́˾˿˲̒́̃˻˱˾˱˵˵˱˾˾̌˽˹̂˲˼˿˻˹́˿˳˻˱˽˹
templatetypename 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
149. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿˲̒́̃˻˱˾˱˵˵˱˾˾̌˽˹̂˲˼˿˻˹́˿˳˻˱˽˹
templatetypename 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_guardstd::mutex lock(m);
// вызов “объявления” под защитой мьютекса
return f(t);
}
};
114
150. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿˲̒́̃˻˱˾˱˵˵˱˾˾̌˽˹̂˲˼˿˻˹́˿˳˻˱˽˹
monitorstd::string smon{start }; // инициализация
std::vectorstd::futurevoid 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 donen;
115
151. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿˲̒́̃˻˱˾˱˵˵˱˾˾̌˽˹̂˲˼˿˻˹́˿˳˻˱˽˹
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 = 3
done 116
152. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿˲̒́̃˻˱˾˱˵˵˱˾˾̌˽˹̂˲˼˿˻˹́˿˳˻˱˽˹
monitorstd::ostream mon_cout{std::cout};
std::vectorstd::futurevoid 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 donen;
}); 117
153. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿˲̒́̃˻˱˾˱˵˵˱˾˾̌˽˹̂˲˼˿˻˹́˿˳˻˱˽˹
i = 0
i = 2
hi from 2
hi from 0
i = 1
hi from 1
i = 3
hi from 3
i = 4
hi from 4
done
i = 0
i = 3
i = 2
hi from 2
i = 1
hi from 1
hi from 3
i = 4
hi from 4
hi from 0
done
i = 0
hi from 0
i = 4
hi from 4
i = 2
hi from 2
i = 3
hi from 3
i = 1
hi from 1
done
i = 0
hi from 0
i = 2
hi from 2
i = 3
hi from 3
i = 1
hi from 1
i = 4
hi from 4
done
118
155. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿˲̒́̃˻˱˾˱˵˵˱˾˾̌˽˹˾˱˿̂˾˿˳˶˿̈˶́˶˵˹˸˱˵˱̈
concurrentstd::string smon{start }; // инициализация
std::vectorstd::futurevoid 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 donen;
120
156. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿˲̒́̃˻˱˾˱˵˵˱˾˾̌˽˹˾˱˿̂˾˿˳˶˿̈˶́˶˵˹˸˱˵˱̈
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
done
start 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
157. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿˲̒́̃˻˱˾˱˵˵˱˾˾̌˽˹˾˱˿̂˾˿˳˶˿̈˶́˶˵˹˸˱˵˱̈
concurrentstd::ostream mon_cout{std::cout};
std::vectorstd::futurevoid 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 donen;
}); 122
158. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿˲̒́̃˻˱˾˱˵˵˱˾˾̌˽˹˾˱˿̂˾˿˳˶˿̈˶́˶˵˹˸˱˵˱̈
i = 0
hi from 0
i = 2
hi from 2
i = 3
hi from 3
i = 4
hi from 4
i = 1
hi from 1
done
i = 0
hi from 0
i = 2
hi from 2
i = 3
hi from 3
i = 4
hi from 4
i = 1
hi from 1
done
i = 0
i = 1
hi from 1
hi from 0
i = 2
hi from 2
i = 4
hi from 4
i = 3
hi from 3
done
i = 0
hi from 0
i = 2
hi from 2
i = 1
i = 3
hi from 3
hi from 1
i = 4
hi from 4
done
123
159. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿˲̒́̃˻˱˾˱˵˵˱˾˾̌˽˹˾˱˿̂˾˿˳˶˿̈˶́˶˵˹˸˱˵˱̈
Данная версия operator() позволяет вернуть значение при вызове
функции:
templatetypename F
auto operator()(F f) - std::futuredecltype(f(t)) {
// создаём объект promise (shared_ptrpromise)
auto p = std::make_sharedstd::promisedecltype(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;
}
124
160. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿˲̒́̃˻˱˾˱˵˵˱˾˾̌˽˹˾˱˿̂˾˿˳˶˿̈˶́˶˵˹˸˱˵˱̈
templatetypename F
auto operator()(F f) - std::futuredecltype(f(t)) {
auto p = std::make_sharedstd::promisedecltype(f(t))();
auto ret = p-get_future();
q.push([=]{
try { set_value(*p, f, t); }
catch (...)
{ p-set_exception(std::current_exception()); }
});
return ret;
}
templatetypename Fut, typename F, typename T1
void set_value(std::promiseFut p, F f, T1 t)
{ p.set_value(f(t)); }
templatetypename F, typename T1
void set_value(std::promisevoid p, F f, T1 t)
{ f(t); p.set_value(); }
125
161. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿˲̒́̃˻˱˾˱˵˵˱˾˾̌˽˹˾˱˿̂˾˿˳˶˿̈˶́˶˵˹˸˱˵˱̈
templatetypename F
auto operator()(F f) - std::futuredecltype(f(t)) {
auto p = std::make_sharedstd::promisedecltype(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 += donen;
std::cout s;
return s;
});
std::cout return value: f.get() std::endl; 126
162. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿˲̒́̃˻˱˾˱˿̂˾˿˳˶˿̈˶́˶˵˹˸˱˵˱̈̀́˹˽˶˾˶˾˹˶
class backgrounder {
public:
futurebool save(std::string file) {
c([=](data d) {
... // каждая функция - в отдельной транзакции
});
}
futuresize_t print(some stuff) {
c([=, stuff](data d) {
... // атомарный неделимый вывод
});
}
private:
struct data { /* ... */ } // данные
concurrentdata c; // обёртка для потокобезопасного
}; // выполнения операций с данными
127
165. ˧˶˼̍́˱˸́˱˲˿̃˻˹̀˱́˱˼˼˶˼̍˾̌̆̂̃́̄˻̃̄́˵˱˾˾̌̆
Задачи проектирования структур данных с блокировками:
▪ Ни один поток не может увидеть состояние, в котором
инварианты нарушены
▪ Предотвратить состояние гонки
▪ Предусмотреть возникновение исключений
▪ Минимизировать возможность взаимоблокировок
Средства достижения:
▪ ограничить область действия блокировок
▪ защитить разные части структуры разными
мьютексами
▪ обеспечить разный уровень защиты
▪ изменить структуру данных для расширения
возможностей распраллеливания 130
179. ˟̈˶́˶˵̍̂˿˷˹˵˱˾˹˶˽˲˶˸˿̀˱̂˾˿̂̃̍˹̂˻˼̏̈˶˾˹˺
templatetypename T class threadsafe_queue {
private:
mutable std::mutex mut;
std::queueT data_queue;
std::condition_variable data_cond;
public:
threadsafe_queue() {}
void push(T new_value) {
std::lock_guardstd::mutex lk(mut);
data_queue.push(std::move(new_value));
data_cond.notify_one();
}
std::shared_ptrT wait_and_pop() {
std::unique_lockstd::mutex lk(mut);
data_cond.wait(lk, [this]{return !data_queue.empty();});
std::shared_ptrT res(
std::make_sharedT(std::move(data_queue.front())));
data_queue.pop();
return res;
}
При срабатывании
исключения
в wait_and_pop (в ходе
инициализации res)
другие потоки не будут
разбужены
144
180. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿̈˶́˶˵̍˽˿˵˹̅˹̇˹́˿˳˱˾˾˱̐˳˶́̂˹̐
templatetypename T class threadsafe_queue {
private:
mutable std::mutex mut;
std::queuestd::shared_ptrT data_queue;
std::condition_variable data_cond;
public:
void push(T new_value) {
std::shared_ptrT data(
std::make_sharedT(std::move(new_value)));
std::lock_guardstd::mutex lk(mut);
data_queue.push(std::move(new_value));
data_cond.notify_one();
}
std::shared_ptrT wait_and_pop() {
std::unique_lockstd::mutex lk(mut);
data_cond.wait(lk, [this]{ return !data_queue.empty(); });
std::shared_ptrT res = data_queue.front();
data_queue.pop();
return res;
}
Очередь теперь
хранит элементы
shared_ptr
Инициализация
объекта теперь
выполняется не под
защитой блокировки
(и это весьма хорошо)
Объект извлекается
напрямую 145
181. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿̈˶́˶˵̍˽˿˵˹̅˹̇˹́˿˳˱˾˾˱̐˳˶́̂˹̐
void wait_and_pop(T value) {
std::unique_lockstd::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_guardstd::mutex lk(mut);
if (data_queue.empty()) return false;
value = std::move(*data_queue.front());
data_queue.pop();
return true;
}
std::shared_ptrT try_pop() {
// ...
}
bool empty() const { /* ... */ }
Объект
извлекается из
очереди напрямую,
shared_ptr не
инициализируется
- исключение не
возбуждается!
146
182. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿̈˶́˶˵̍˽˿˵˹̅˹̇˹́˿˳˱˾˾˱̐˳˶́̂˹̐
std::shared_ptrT wait_and_pop() {
std::unique_lockstd::mutex lk(mut);
data_cond.wait(lk, [this]{ return !data_queue.empty(); });
std::shared_ptrT res = data_queue.front();
data_queue.pop();
return res;
Недостатки реализации:
▪ Сериализация потоков приводит к снижению
}
bool try_pop(T value) {
std::lock_guardstd::mutex lk(mut);
if (data_queue.empty()) return false;
value = std::move(*data_queue.front());
data_queue.pop();
return true;
}
std::shared_ptrT try_pop() {
// ...
}
bool empty() const { /* ... */ }
Объект извлекается
из очереди
напрямую,
shared_ptr не
инициализируется
производительности: потоки простаивают и не
совершают полезной работы
147
186. ˟̈˶́˶˵̍̂˽˶˼˻˿˸˶́˾˹̂̃̌˽˹˲˼˿˻˹́˿˳˻˱˽˹
std::shared_ptrT try_pop() {
if (!head) {
return std::shared_ptrT();
}
std::shared_ptrT const res(
std::make_sharedT(std::move(head-data)));
std::unique_ptrnode const old_head = std::move(head);
head = std::move(old_head-next);
return res;
}
void push(T new_value) {
std::unique_ptrnode 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
187. ˟̈˶́˶˵̍̂˽˶˼˻˿˸˶́˾˹̂̃̌˽˹˲˼˿˻˹́˿˳˻˱˽˹
std::shared_ptrT try_pop() {
if (!head) {
return std::shared_ptrT();
}
std::shared_ptrT const res(
std::make_sharedT(std::move(head-data)));
std::unique_ptrnode const old_head = std::move(head);
head = std::move(old_head-next);
return res;
}
void push(T new_value) {
std::unique_ptrnode 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
189. ˝˿˵˹̅˹̇˹́˿˳˱˾˾˱̐˳˶́̂˹̐
Head Tail
Фиктивный узел
▪ При пустой очереди head и tail указывают на фиктивный
узел, а не равны NULL, причём head == tail.
▪ При очереди с одним элементом head-next и tail-next
указывают на разные узлы (причём head-next == tail), в
результате чего гонки не возникает. 154
190. ˠ̄̂̃˱̐˿̈˶́˶˵̍
Head Tail
Фиктивный узел
▪ При пустой очереди head и tail указывают на фиктивный
узел, а не равны NULL, причём head == tail.
155
191. ˟̈˶́˶˵̍̂˿˵˾˹˽̎˼˶˽˶˾̃˿˽
Head Tail
Фиктивный узел
▪ При пустой очереди head и tail указывают на фиктивный
узел, а не равны NULL, причём head == tail.
▪ При очереди с одним элементом head-next и tail-
next указывают на разные узлы (причём head-next ==
tail), в результате чего гонки не возникает. 156
192. ˟̈˶́˶˵̍̂˽˶˼˻˿˸˶́˾˹̂̃̌˽˹˲˼˿˻˹́˿˳˻˱˽˹
templatetypename T
class queue {
private:
struct node {
std::shared_ptrT data;
std::unique_ptrnode next;
};
std::unique_ptrnode head;
node *tail;
public:
указатель на данные
вместо данных
node хранит указатель
на данные
создание первого фиктивного узла в конструкторе
queue(): head(new node), tail(head.get()) {}
queue(const queue other) = delete;
queue operator=(const queue other) = delete;
▪ Вводится фиктивный узел
▪ При пустой очереди head и tail теперь
указывают на фиктивный узел, а не на NULL
157
193. ˟̈˶́˶˵̍̂˽˶˼˻˿˸˶́˾˹̂̃̌˽˹˲˼˿˻˹́˿˳˻˱˽˹
std::shared_ptrT try_pop() {
if (head.get() == tail) {
return std::shared_ptrT();
}
std::shared_ptrT const res(head-data);
std::unique_ptrnode old_head = std::move(head);
head = std::move(old_head-next);
return res;
}
void push(T new_value) {
std::shared_ptrT new_data(
std::make_sharedT(std::move(new_value)));
std::unique_ptrnode 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
206. ˟̈˶́˶˵̍̂˽˶˼˻˿˸˶́˾˹̂̃̌˽˹˲˼˿˻˹́˿˳˻˱˽˹
std::shared_ptrT try_pop() {
if (head.get() == tail) {
return std::shared_ptrT();
}
std::shared_ptrT const res(head-data);
std::unique_ptrnode old_head = std::move(head);
head = std::move(old_head-next);
return res;
}
void push(T new_value) {
std::shared_ptrT new_data(
try_pop
обращается
только к head
std::make_sharedT(std::move(new_value)));
std::unique_ptrnode p(new node);
tail-data = new_data;
node *const new_tail = p.get();
tail-next = std::move(p);
tail = new_tail;
}
обращение к tail
только на момент
начального сравнения
push
обращается
только к tail
165
207. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿̈˶́˶˵̍̂˽˶˼˻˿˸˶́˾˹̂̃̌˽˹˲˼˿˻˹́˿˳˻˱˽˹
1 2
Head Tail
▪ Функция push обращается только к tail, try_pop -
только к head (и tail на короткое время).
▪ Вместо единого глобального мьютекса можно завести два
отдельных и удерживать блокировки при доступке к head
и tail.
166
208. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿̈˶́˶˵̍̂˽˶˼˻˿˸˶́˾˹̂̃̌˽˹˲˼˿˻˹́˿˳˻˱˽˹
templatetypename T class queue {
private:
struct node {
std::shared_ptrT data;
std::unique_ptrnode next;
};
std::mutex head_mutex, tail_mutex;
std::unique_ptrnode head;
node *tail;
node *get_tail() {
std::lock_guardstd::mutex tail_lock(tail_mutex);
return tail;
}
std::unique_ptrnode pop_head() {
std::lock_guardstd::mutex head_lock(head_mutex);
if (head.get() == get_tail()) return nullptr
std::unique_ptrnode old_head = std::move(head);
head = std::move(old_head-next);
return old_head;
}
блокируется только на момент
получения элемента tail
167
209. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿̈˶́˶˵̍̂˽˶˼˻˿˸˶́˾˹̂̃̌˽˹˲˼˿˻˹́˿˳˻˱˽˹
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_ptrT try_pop() {
std::unique_ptrnode old_head = pop_head();
return old_head ? old_head-data : std::shared_ptrT();
}
void push(T new_value) {
std::shared_ptrT new_data(
std::make_sharedT(std::move(new_value)));
std::unique_ptrnode p(new node);
node* const new_tail = p.get();
std::lock_guardstd::mutex tail_lock(tail_mutex);
tail-data = new_data;
tail-next = std::move(p);
tail = new_tail;
}
};
push обращается только к
tail, но не к head, поэтому
используется одна блокировка
168
212. ˠ˿̃˿˻˿˲˶˸˿̀˱̂˾˱̐˿̈˶́˶˵̍̂˽˶˼˻˿˸˶́˾˹̂̃̌˽˹˲˼˿˻˹́˿˳˻˱˽˹˹̂˿˷˹˵˱˾˹˶˽
̀˿̂̃̄̀˼˶˾˹̐̎˼˶˽˶˾̃˱
Особенности реализации:
▪ Освободить мьютекс в push до вызова notify_one,
чтобы разбуженный поток не ждал освобождения
мьютекса.
▪ Проверку условия можно выполнять под защитой
head_mutex, захватывая tail_mutex только для
чтения tail. Предикат выглядит как head !=
get_tail()
▪ Для версии pop, работающей со ссылкой,
необходимо переопределить wait_and_pop(),
чтобы обеспечить безопасность с точки зрения
исключений. Необходимо сначала скопировать
данные из узла, а потом удалять узел из списка.
171