ЛЕКЦИЯ 5. Шаблоны многопоточного программирования
Курс "Параллельные вычислительные технологии" (ПВТ), весна 2015
Сибирский государственный университет телекоммуникаций и информатики
Пазников Алексей Александрович
к.т.н., доцент кафедры вычислительных систем СибГУТИ
http://cpct.sibsutis.ru/~apaznikov
ПВТ - весна 2015 - Лекция 5. Многопоточное программирование в С++. Синхронизация, будущие результаты
1. Лекция 5. Многопоточное
программирование в языке С++.
Работа с потоками. Защита данных.
Синхронизация. Будущие результаты
Пазников Алексей Александрович
Кафедра вычислительных систем СибГУТИ
Сайт курса: http://cpct.sibsutis.ru/~apaznikov/teaching/
Q/A: https://piazza.com/sibsutis.ru/spring2015/pct2015spring
Параллельные вычислительные технологии
Весна 2015 (Parallel Computing Technologies, PCT 15)
9. Запуск нескольких потоков и ожидание завершения
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
10. Запуск нескольких потоков и идентификаторы потоков
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
31. Мьютексы в С++
class wrapper {
private:
data_t data; // защищаемые данные
std::mutex mut;
public:
template<typename Function>
void proc_data(Function func) {
std::lock_guard<std::mutex> lock(mut);
func(data); }
};
data_t *unprotected; // внешний указатель
void unsafe_func(data_t &protected) {
unprotected = &protected;
}
wrapper obj;
obj.proc_data(unsafe_func);
unprotected->do_something(); // незащищённый доступ к data
Любой код,
имеющий
доступ к
указателю или
ссылке, может
делать с ним
всё, что угодно,
не захватывая
мьютекс.
31
32. Мьютексы в С++
class wrapper {
private:
data_t data; // защищаемые данные
std::mutex mut;
public:
template<typename Function>
void proc_data(Function func) {
std::lock_guard<std::mutex> lock(mut);
func(data); }
};
data_t *unprotected; // внешний указатель
void unsafe_func(data_t &protected) {
unprotected = &protected;
}
wrapper obj;
obj.proc_data(unsafe_func);
unprotected->do_something(); // незащищённый доступ к data
Любой код,
имеющий
доступ к
указателю или
ссылке, может
делать с ним
всё, что угодно,
не захватывая
мьютекс.
Нельзя передавать указатели и
ссылки на защищённые данные
за пределы области видимости
блокировки никаким образом.
32
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&&);
};
некорректный
результат
как решить?
stack<int> s;
if (!s.empty()) {
int const value = s.top();
s.pop();
// ...
}
35
36. Адаптация интерфейсов к многопоточности
std::vector<int> result;
mystack.pop(result);
1. Передавать ссылку в функцию pop
2. Потребовать наличия копирующего или перемещающего
конструктора, не возбуждающего исключений (доказано, что
можно объединить pop и top, но это можно сделать только если
конструкторы не вызывают исключений)
3. Возвращать указатель на вытолкнутый элемент
4. Одновременно 1 и один из вариантов 2 или 3
std::shared_ptr<T> pop()
36
45. Блокировка с помощью std::unique_lock
class Widget {
int val;
std::mutex m;
int getval() const {
return val;
}
};
bool Cmp(Widget &lhs, Widget &rhs) {
// не захватываем пока мьютексы
std::unique_lock<std::mutex> lock1(lhs.m,std::defer_lock);
std::unique_lock<std::mutex> lock2(rhs.m,std::defer_lock);
// а вот сейчас захватываем, причём без дедлоков
std::lock(lock1, lock2);
return lhs.getval() > rhs.getval() ? true : false;
}
45
46. Блокировка с помощью std::unique_lock
class Widget {
int val;
std::mutex m;
int getval() const {
std::lock_guard<std::mutex> lock(m);
return val;
}
};
bool Cmp(Widget &lhs, Widget &rhs) {
// обе операции совершаются под защитой мьютекса
int const lhs_val = lhs.getval();
int const rhs_val = rhs.getval();
std::lock(lock1, lock2);
return lhs_val > rhs_val ? true : false;
}
Минимизация
гранулярности
блокировки!
46
47. Блокировка с помощью std::unique_lock
void pop_and_process() {
std::unique_lock<std::mutex> lock(mut);
Widget data = queue.pop(); // получить элемент данных
lock.unlock(); // освободить мьютекс
super_widget result = process(data); // обработать данные
lock.lock(); // опять захватить мьютекс
output_result(data, result); // вывести результат
}
Минимизация блокировок!
▪ блокировать данные, а не операции
▪ удерживать мьютекс столько, сколько необходимо
▫ тяжёлые операции (захват другого мьютекса,
ввод/вывод и т.д.) - вне текущей критической секции
47
48. Однократный вызов и отложенная инициализация
class NetFacility {
private:
connect_handle connection;
bool connection_flag;
void open_connection() {
connection = connect_manager.open();
}
public:
NetFacility(connect_info &_info): {}
void send_data(data_packet const &d) {
// отложенная инициализация
if (connection_flag == false)
connection = open_connection();
connection.send(data);
}
void recv_data() { /* ... */ }
} А если несколько
потоков?
48
51. Однократный вызов и отложенная инициализация
class NetFacility {
private:
connect_handle connection;
std::once_flag connection_flag;
void open_connection() {
connection = connect_manager.open(info);
}
public:
NetFacility(connect_info &_info): {}
void send_data(data_packet const &d) {
// вызывается только один раз
std::call_once(connection_flag,
&NetFacility::open_connection, this);
connection.send(data);
}
void recv_data() { /* ... */ }
}
51
52. R/W-мьютексы в С++
class Widget {
mutable std::shared_timed_mutex mut;
int data;
public:
Widget& operator=(const R& rhs) {
// эксклюзивные права на запись в *this
std::unique_lock<std::shared_timed_mutex>
lhs(mut, std::defer_lock);
// разделяемые права на чтение rhs
std::shared_lock<std::shared_timed_mutex>
rhs(other.mut, std::defer_lock);
std::lock(lhs, rhs);
// выполнить присваивание
data = rhs.data;
return *this;
}
};
52
53. R/W-мьютексы в С++
int Widget::read() {
std::shared_lock<shared_timed_mutex> lock(mut);
return val;
}
void Widget::set_value(int _val) {
std::lock_guard<shared_mutex> lock(mut);
val = _val;
}
53
54. Рекурсивные мьютексы
▪ std::recursive_mutex
▪ мьютекс можно запирать несколько раз в
одном потоке
▪ освобождать мьютекс требуется столько
раз, сколько он был захвачен
▪ использование - аналогично std::mutex
(std::lock_guard, std::unique_lock,
…)
54
55. Условные переменные
▪ std::condition_variable, std::
condition_variable_any
условная переменная, необходимо взаимодействие с
мьютексом (condition_variable) или с любым
классом (condition_variable_any), подобным
мьютексу
▪ wait - ожидание условия
▪ wait_for, wait_until - ожидание условия заданное
время или до заданного момента
▪ notify_one - сообщить одному потоку
▪ notify_all - сообщить всем потокам
55
59. Будущие результаты (future)
int thinking();
// Запуск асинхронной (“фоновой”) задачи
std::future<int> answer = std::async(thinking);
// Работа основного потока
do_other_stuff(); // в этом время работает thinking()
// Получение результатов
std::cout << "The answer is " << answer.get() << std::endl;
T1main thread
работа ожидание
T2thinking...
answer.get()
async
59
60. Будущие результаты (future)
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);
60
61. Будущие результаты (future)
struct Widget {
Widget();
Widget(Widget&&); // Конструктор перемещения
Widget(Widget const&) = delete; // Запретить копирование
// Оператор “перемещающее присваивание”
Widget& operator=(Widget&&);
// Запретить присваивание
Widget& operator=(Widget const&) = delete;
void foo(std::string const&, int);
int bar(std::string const&);
int operator()(int);
};
Widget w;
auto f1 = std::async(&Widget::foo, &w, "hi", 2014);
auto f2 = std::async(&Widget::bar, w, "hi");
auto f3 = std::async(Widget(), 2014);
auto f4 = std::async(std::ref(w), 2014);
61
62. Будущие результаты (future)
▪ std::launch::async - запуск функции в
асинхронном режиме
▪ std::launch::deferred - запуск в момент вызова
wait или get
▪ std::launch::async | std::launch::deferred -
на усмотрение реализации (по умолчанию)
auto f5 = std::async(std::launch::deferred,
Widget::foo(), "carpe diem", 2014);
auto f6 = std::async(std::launch::deferred,
Widget::bar(), "carpe diem");
auto f7 = std::async(std::launch::async, Widget(), 2014);
std::cout << f5.get() << std::endl; // вызывается foo()
f6.wait(); // вызывается bar()
std::cout << f7.get() << std::endl; // только ожидание
// результата 62
64. Упакованные задачи
▪ Шаблон std::packaged_task<> связывается будущий
результат (future) с функцией
▪ Вызов функции происходит при вызове объекта
packaged_task
▪ Параметр шаблона - сигнатура функции
template<>
class packaged_task<int(float, char)> {
public:
template<typename Callable>
explicit packaged_task(Callable &func);
std::future<int> get_future();
void operator()(std::vector<char>*, int);
};
пример
спецификации
шаблона для
сигнатуры
функции
int func(float,
char)
64
65. Упакованные задачи - пример (пул задач)
task
package
task
package
tasks.push_back(
std::move(task));
std::packaged_task<void()> task
= std::move(tasks.front());
batch_systemadd_task
65
task()
66. Упакованные задачи - пример
std::mutex mut;
std::deque<std::packaged_task<void()>> tasks;
bool exit_flag = false;
bool is_exit() {
std::mutex mut;
std::lock_guard<std::mutex> lock(mut);
return exit_flag;
}
void batch_system() {
while (!is_exit()) {
std::unique_lock<std::mutex> lock(mut);
if (tasks.empty()) continue;
std::packaged_task<void()> task = // получить упакованную
std::move(tasks.front()); // задачу из очереди
tasks.pop_front(); // удалить из очереди
lock.unlock();
task(); // запуск задачи
} } 66
72. “Обещанные” результаты (std::promise) - пример 1
void print_value(std::future<int>& fut) {
int x = fut.get();
std::cout << "value: " << x << std::endl;
}
int compute_value() {
std::this_thread::sleep_for(std::chrono::seconds(1));
return 42;
}
int main () {
std::promise<int> prom;
// Получаем объект future из созданного promise (обещаем)
std::future<int> fut = prom.get_future();
// Отправляем будущее значение в новый поток
std::thread th1 (print_value, std::ref(fut));
int val = compute_value();
prom.set_value(val); // Выполняем обещание
th1.join();
} 72
73. “Обещанные” результаты (std::promise) - пример 1
mainmain thread
th1print_value
prom.set_value()
print_value
th1(print_value,
std::ref(fut))
fut.get()
compute_value
работа ожидание
создание/завершение
потоков
синхронизация
73
74. “Обещанные” результаты (std::promise) - пример 2
int main() {
std::istringstream iss_numbers{"3 1 42 23 -23 93 2 -289"};
std::istringstream iss_letters{" a 23 b,e k k?a;si,ksa c"};
std::vector<int> numbers;
std::vector<char> letters;
std::promise<void> numbers_promise, letters_promise;
auto numbers_ready = numbers_promise.get_future();
auto letter_ready = letters_promise.get_future();
std::thread value_reader([&]{
std::copy(std::istream_iterator<int>{iss_numbers},
std::istream_iterator<int>{},
std::back_inserter(numbers));
numbers_promise.set_value();
std::copy_if(std::istreambuf_iterator<char>{iss_letters},
std::istreambuf_iterator<char>{},
std::back_inserter(letters), ::isalpha);
letters_promise.set_value();
}); 74
75. “Обещанные” результаты (std::promise) - пример 2
numbers_ready.wait(); // Ждать когда числа будут готовы
std::sort(numbers.begin(), numbers.end());
if (letter_ready.wait_for(std::chrono::milliseconds(100)) ==
std::future_status::timeout) {
// выводим числа, пока обрабатываются символы
for (int num : numbers) std::cout << num << ' ';
std::cout << 'n';
numbers.clear(); // Числа уже были напечатаны
}
letter_ready.wait();
std::sort(letters.begin(), letters.end());
for (char let : letters) std::cout << let << ' ';
std::cout << 'n';
// If numbers were already printed, it does nothing.
for (int num : numbers) std::cout << num << ' ';
std::cout << 'n';
value_reader.join();
}
75
76. “Обещанные” результаты (std::promise) - пример 2
mainmain
работа ожидание
value_
reader
letters_promise.
set_value()value_reader
fut.get()
iss_numbers iss_letters
number_ready.wait()
sort
letter_ready.wait_for
sort output
numbers_promise.
set_value()
создание/завершение
потоков
синхронизация
76
77. “Обещанные” результаты (std::promise), варианты
a a a a b c e i k k k s s
-289 -23 1 2 3 4 23 42 93 93
-289 -23 1 2 3 23 42 93
a a a b c e i k k k s s
77
80. Проблемы с параллелизмом на основе потоков
int doWork();
std::thread t(doWork); // 1
// или
auto fut = std::async(doWork); // 2
80
▪ Вариант, основанный на задаче (2), предпочтительней,
т.к. предполагает возвращаемое значение, которое
можно получить fut.get().
▪ Если doWork выбрасывает исключение, то get()
позволяет обработать исключения, в то время как в
первом случае выброс исключения приведёт к
завершению программы.
81. Проблемы с параллелизмом на основе потоков
81
Параллелизм задач находится на более высоком уровне
абстракции по сравнению с параллелизмом потоков,
освобождает программиста от деталей реализации:
▪ Аппаратные потоки (software threads) - те, которые
действительно выполняют вычисления (по числу ядер).
▪ Программные потоки (hardware threads) - потоки,
которые планируются ОС и выполняются на аппаратных
потоках.
▫ Легковесные потоки (lightweight threads) - потоки,
которые выполняются целиком в пространстве
пользователя.
▪ std::thread - объекты С++, которые соответствуют
определённым программным потокам, с которыми
можно выполнять операции join и detach
82. Ограниченность количества программных потоков
82
Программные потоки - ограниченный ресурс. Попытка
создать больше заданного числа потоков вызовет
исключение, даже если
int doWork() noexcept;
std::thread t(doWork); // может быть исключение!
▪ Запустить doWork в текущем потоке?
▪ Или подождать, пока освободится программный поток?
:(
83. Перегруженность аппаратных потоков (oversubscription)
83
Состояние перегруженности аппаратных потоков
oversubscription возникает, когда в системе большое
количество runnable-потоков.
Планировщик ОС выделяет программным потокам порции
(time-slice) процессорного времени.
После окончания порции происходит переключение
контекста (context switch), особенно в случае, когда поток
назначается на разные ядра:
▪ Кэш-память не загружена, большое количество
промахов по кэшу.
▪ Запуск нового потока на ядре перезаписывает записи
для старого потока, который, вероятно, будет опять
назначен на это ядро. Это опять приводит к промахам
по кэшу.
84. Перегруженность аппаратных потоков (oversubscription)
84
Выбор оптимального количества потоков для избежания
перегруженности зависит от:
▪ Момента, когда программа переходит из региона с
вводом-выводом к области с вычислениями.
▪ Стоимости переключения контекста
▪ Того, насколько эффективно потоки используют кэш
▪ Аппаратной архитектуры
85. Перегруженность аппаратных потоков (oversubscription)
85
Выбор оптимального количества потоков для избежания
перегруженности зависит от:
▪ Момента, когда программа переходит из региона с
вводом-выводом к области с вычислениями.
▪ Стоимости переключения контекста
▪ Того, насколько эффективно потоки используют кэш
▪ Аппаратной архитектуры
Сделай жизнь легче, используй std::async!
Ответственность за управление потоками
лежит на плечах разработчика стандартной
библиотеки!
86. Long live std::async!
86
std::async позволяет создать неограниченное количество
асинхронных функций
▪ Вызов std::async не гарантирует создание нового
программного потока (политики async и deferred).
▪ Асинхронная функция может быть запущена, например,
в том же потоке, где и вызывается get, wait,позволяя
избежать перегруженности (oversubscription).
▪ Возможность work-stealing (легковесных потоков)
87. Long live std::async! But...
87
std::async позволяет создать неограниченное количество
асинхронных функций
▪ Вызов std::async не гарантирует создание нового
программного потока (политики async и deferred).
▪ Асинхронная функция может быть запущена, например,
в том же потоке, где и вызывается get, wait,позволяя
избежать перегруженности (oversubscription).
▪ Возможность work-stealing (легковесных потоков)
Но std::async не универсальное средство. Недостатки:
▪ Приводит к возможному дисбалансу загрузки.
Планирование происходит на двух уровнях: ОС и
программы.
▪ Не подходит для некоторых целей (напр., GUI)
88. Когда таки нужно использовать потоки
88
▪ Нужно воспользоваться функционалом
низкоуровневой реализации потоков (например, std::
thread::native_handle)
▪ Нужно оптимизировать использвоание потоков в
программе. Допустим, если вы разрабатываете
сервер, который будет запускаться на заданной
архитектуре.
▪ Вы хотите реализовать потоки на архитектуре, где
пока нет реализации C++-concurrency.
Но это редко. В большинстве случаев смело
разрабатывайте программы на основе задач,
а не на основе потоков.
94. Помните про политику запуска задачи
94
▪ std::launch::async - функция будет запущена
асинхронного, т.е. в отдельном потоке
T1main thread
T2func
fut.get()
▪ std::launch::deferred - функция может быть
запущена только, когда вызан методв get или wait
для объекта future в потоке, вызывающем get (wait).
T1main thread
T2func
fut.get()
async
async
95. Помните про политику запуска задачи
95
▪ std::launch::async - функция будет запущена
асинхронного, т.е. в отдельном потоке
▪ std::launch::deferred - функция может быть
запущена только, когда вызан методв get или wait
для объекта future в потоке, вызывающем get (wait).
auto fut = std::async(func); // использовать политику
// запуска по умолчанию
▪ Нельзя предугадать, будет ли func выполняться
асинхронно
▪ Нельзя предугадать, будет ли func запущена на
потоке, отличном, от потока, вызывающего get (wait)
▪ Нельзя предугадать, что func будет выполнена.
96. Проблема: std::async и Thread Local Storage (TLS)
96
auto fut = std::async([](){ // Может использоваться
thread_local local_var; // TLS для независимого
... // потока
});
...
fut.get(); // а может и для этого!
Политика запуска по умолчанию конфликтует с
использованием переменных thread_local:
97. Проблема: std::async и цикла на основе wait_for
97
using namespace std::literals; // C++14 суффиксы
auto fut = std::async([]() {
std::this_thread::sleep_for(1s);
});
// Цикл, ожидающий выполнения std::async,
// может не завершиться
while (fut.wait_for(100ms) !=
std::future_status::ready)
{ /* делать что-то асинхронного */}
Использование циклов на основе вызова wait_for или
wait_until может привести к вечному ожиданию, если
задача будет запущена как отложенная (std::launch::
deferred):
98. Проблема: std::async и цикла на основе wait_for - решение
98
auto fut = std::async([]() {
std::this_thread::sleep_for(1s);
});
if (fut.wait_for(0s) ==
std::future_satus::deferred) {
fut.get(); // ожидаем результата
...
} else {
while (fut.wait_for(100ms) !=
std::future_status::ready) {
// делать какую-то работу асинхронного,
// пока ждём завершения выполнения задачи
}
// здесь fut готово
}
99. Когда использовать политику запуска по умолчанию
99
▪ Задача не требует асинхронного запуска в
отдельном потоке, отличном от вызывающего get
(wait).
▪ Не важно, thread_local-переменные какого потока
будут использоваться.
▪ Или есть гарантия, что get (wait) будут вызваны для
объекта future, возвращённого std::async, или
задача может быть вовсе на запущена.
▪ При использовании wait_for или wait_until
допускается возможность отложенного запуска
задачи.
Если какие-либо пункты не выполняются, лучше
гарантировать асинхронный запуск задачи через
передачу std::launch::async
101. Joinable и unjoinable
101
Объект std::thread может пребывать в двух состояниях:
▪ joinable: объект соответствует потоку, который
выполняется или может быть запущен.
▪ unjoinable: объект, с которым нельзя выполнить
операцию join:
▫ выполнен конструктур по умолчанию для std::
thread, т.е. std::thread может не имеет функции
для выполнения и поэтому
не соответствует реальному потоку.
▫ std::thread, который был перемещён (moved)
▫ std::thread, который был присоединён (joined)
▫ std::thread, который был отсоденинён (detached)
102. Проблема с joinable-потоками
102
constexpr auto n = 10'000'000; // C++14-style
bool doWork(std::function<bool(int)> pred, // условие
int maxVal = n) {
std::vector<int> goodVals; // значения, удовл. условию
std::thread t([&pred, maxVals, &goodVals]{
for (auto i = 0; i <= maxVals; i++) {
if (pred(i)) goodVals.push_back(i); }
});
auto nh = t.native_handle();
... // низкоуровневые манипуляции с потоком
if (conditionAreSatisfied()) {
t.join();
performComputation(goodVals);
return true;
}
return false;
}
103. Проблема с joinable-потоками
103
constexpr auto n = 10'000'000; // C++14-style
bool doWork(std::function<bool(int)> pred, // условие
int maxVal = n) {
std::vector<int> goodVals; // значения, удовл. условию
std::thread t([&pred, maxVals, &goodVals]{
for (auto i = 0; i <= maxVals; i++) {
if (pred(i)) goodVals.push_back(i); }
});
auto nh = t.native_handle();
...
if (conditionAreSatisfied()) {
t.join();
performComputation(goodVals);
return true;
}
return false;
}
sched_param sch;
int policy;
pthread_getschedparam(nh, &policy, &sch);
sch.sched_priority = 20;
if (pthread_setschedparam(nh, SCHED_FIFO, &sch)) {
std::cout << "Failed to setschedparam: "
<< std::strerror(errno) << 'n';
}
104. Проблема с joinable-потоками
constexpr auto n = 10'000'000; // C++14-style
bool doWork(std::function<bool(int)> pred, // условие
int maxVal = n) {
std::vector<int> goodVals; // значения, удовл. условию
std::thread t([&pred, maxVals, &goodVals]{
for (auto i = 0; i <= maxVals; i++) {
if (pred(i)) goodVals.push_back(i); }
});
auto nh = t.native_handle();
...
if (conditionAreSatisfied()) {
t.join();
performComputation(goodVals);
return true; // ok, т.к. был t.join()
}
return false; // не было t.join()! выброс исключения
} // и аварийное завершение программы 104
105. Проблема с joinable-потоками
Как можно решить проблему (наивно):
▪ Неявный join. Деструктор std::thread будет ожидать
завершения потока. Но это приведёт к неочевидному
коду, например, когда поток ждёт завершения doWork,
уже зная, что условие не выполнено.
▪ Неявный detach. Деструктор разрывает связь между
std::thread и потоком выполнения. Поток
продолжает работать. В этом случае, например, при
завершении функции doWork, поток продолжает
работать. Этот поток может использовать
автоматические переменные из стека doWork.
Поэтому стандарт запретил уничтожение потока в
сотоянии joinable: деструктур такого объекта вызывает
завершение программы.
105
106. Проблема с joinable-потоками
106
▪ Программист должен следить за тем, что объект
должен находиться в состоянии unjoinable вне его
области видимости.
▪ Обеспечение этого требования - задача непростая,
поскольку требует отслеживания всех выходов из
функции через return, continue, break, goto,
exception.
▪ Необходимо обеспечить выполнение определённого
действия каждый раз при выходе из блока.
107. Make it RAII!
▪ Программист должен следить за тем, что объект
должен находиться в состоянии unjoinable вне его
области видимости.
▪ Обеспечение этого требования - задача непростая,
поскольку требует отслеживания всех выходов из
функции через return, continue, break, goto,
exception.
▪ Необходимо обеспечить выполнение определённого
действия каждый раз при выходе из блока.
Решение: RAII-объекты (Resouce Acquisition Is
Initialization), (к которым относятся std::unique_ptr,
std::shared_ptr, std::lock_guard, std::fstream и
др.), деструктор которых содержит необходимое действие.
107
108. RAII
108
class ThreadRAII {
public:
enum class DestrAction { join, detach };
ThreadRAII(std::thread&& t, DestrAction a):
action{a}, t{std::move(t)} { }
~ThreadRAII() { // действие выполняется в деструкторе
if (t.joinable()) {
if (action == DestrAction::join) {
t.join();
} else {
t.detach();
}
}
}
std::thread& get() { return t; }
private:
DestrAction action; // action in destuctor
std::thread t;
};
109. RAII
109
bool doWork(std::function<bool(int)> pred, int maxVal = n) {
std::vector<int> goodVals; // значения, удовл. условию
ThreadRAII t{ // использовать RAII-объект
std::thread([&stencil, maxVals, &goodVals]{
for (auto i = 0; i <= maxVals; i++) {
if (pred(i)) goodVals.push_back(i); }
}), ThreadRAII::DestrAction::join // действие
}; // в деструкторе
auto nh = t.get().native_handle();
...
if (conditionAreSatisfied()) {
t.get().join();
performComputation(goodVals);
return true;
}
return false;
}
112. Недостатки синхр-ции на основе условных переменных
std::mutex mut;
std::queue<Widget> widget_queue;
std::condition_variable cond;
void producer() {
for (;;) {
Widget const w = get_request();
std::lock_guard<std::mutex> lock(mut);
widget_queue.push(data);
cond.notify_one();
} }
void consumer() {
for (;;) {
std::unique_lock<std::mutex> lock(mut);
cond.wait(lock, []{return !widget_queue.empty();});
Widget w = widget_queue.pop();
lock.unlock();
process(widget);
} }
112
113. Недостатки синхр-ции на основе условных переменных
▪ Необходимость использования мьютекса
std::unique_lock<std::mutex> lock(mut);
cond.wait(lock, ...);
А что, если потоки выполняют код, который не нуждается в
блокировке мьютекса? Например, один поток инициализирует
структуру, после чего сообщает другому, что структура готова.
▪ Пропущенный сигнал
Поток может отправить сигнал (notify_one/all) тогда, когда другой
поток ещё не начал его ожидать.
▪ Ложное пробуждение (spurious wakeup)
Поток может проснуться тогда, когда сигнал не был отправлен Или
когда он был отправлен потока, а затем условие перестало
выполняться. Поэтому нужна дополнительная проверка:
cond.wait(lock, []{return !widget_queue.empty();}));
А что, если поток не может проверить условие?!
113
114. Недостатки синхр-ции на основе условных переменных
std::atomic<bool> flag(false);
...
flag = true;
...
while (!flag); // активное ожидание! :(
...
114
Для решения проблемы ложного пробуждения можно использовать
атомарный флаг:
Или так:
{
flag = true;
cv.notify_one();
}
{
cv.wait(lk, [] { return flag; }); // код “с запашком” :(
}
116. “Обещанные” результаты (std::promise)
std::futurestd::promise<...> p
p.get_future().wait()
p.set_value()
116
ok, let’s move!
▪ Не требует мьютексов
▪ Не нуждается в атомарных флагах
▪ Не использует активного ожидания
▪ Не зависит от порядка выполнения wait, set_value
117. “Обещанные” результаты (std::promise)
std::futurestd::promise<...> p
p.get_future().wait()
p.set_value()
117
ok, let’s move!
▪ Не требует мьютексов
▪ Не нуждается в атомарных флагах
▪ Не использует активного ожидания
▪ Не зависит от порядка выполнения wait, set_value
▪ Надо заботиться о поведении деструктора future
▪ Можно отправить сигнал только один раз
118. “Обещанные” результаты (std::promise) - пример
std::promise<void> p;
void react(); // реакция на условие
void detect() { // обнаружение условия
std::thread t([] {
p.get_future().wait();
react();
});
// делаем что-то // в это время t спит
p.set_value(); // разбудить t
// делаем ещё что-то
t.join();
};
118
119. “Обещанные” результаты (std::promise) - пример
std::promise<void> p;
void react(); // реакция на условие
void detect() { // обнаружение условия
std::thread t([] {
p.get_future().wait();
react();
});
// а что, если здесь возникнет исключение??
p.set_value(); // разбудить t
// делаем ещё что-то
t.join();
};
119
120. “Обещанные” результаты (std::promise) - пример
std::promise<void> p;
void react(); // реакция на условие
void detect() { // обнаружение условия
ThreadRAII t(std::thread([] {
p.get_future().wait();
react();
)});
...
p.set_value(); // разбудить t
// делаем ещё что-то
t.join();
};
120
121. Множественная отправка сигналов
std::promise<void> p;
void detect() {
auto sf = p.get_future().share();
std::vector<std::thread> vt;
for (auto i = 0; i < threadsToRun; i++) {
vt.emplace_back([sf]{ sf.wait();
react(); });
}
// ...
p.set_value();
// ...
for (auto &t: vt) t.join();
};
121
122. Множественная отправка сигналов
std::promise<void> p;
void detect() {
auto sf = p.get_future().share();
std::vector<std::thread> vt;
for (auto i = 0; i < threadsToRun; i++) {
vt.emplace_back([sf]{ sf.wait();
react(); });
}
// ...
p.set_value();
// ...
for (auto &t: vt) t.join();
};
// RAII... 122
123. Множественная отправка сигналов
std::promise<void> p;
void detect() {
auto sf = p.get_future().share();
std::vector<ThreadRAII> vt;
for (auto i = 0; i < nthreads; i++) {
vt.emplace_back(std::move(ThreadRAII{
std::thread([sf]{
sf.wait();
react(); }
), ThreadRAII::DestrAction::join
}));
}
// ...
p.set_value();
};
123
124. Разделяемые будущие результаты shared_future
int main() {
std::promise<void> ready_promise, t1_ready_promise,
t2_ready_promise;
std::shared_future<void>
ready_future(ready_promise.get_future());
std::chrono::time_point<std::chrono::high_resolution_clock>
start;
auto fun1 = [&]() -> std::chrono::duration<double, std::milli>
{
t1_ready_promise.set_value();
ready_future.wait(); // ожидать сигнала из main()
return std::chrono::high_resolution_clock::now() - start;
};
auto fun2 = [&]() -> std::chrono::duration<double, std::milli>
{
t2_ready_promise.set_value();
ready_future.wait(); // ожидать сигнала из main()
return std::chrono::high_resolution_clock::now() - start;
}; 124
125. Разделяемые будущие результаты 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 startn"
<< "Thread 2 received the signal "
<< result2.get().count() << " ms after startn";
}
125
126. Разделяемые будущие результаты shared_future
main
работа ожидание
создание/завершение
потоков
синхронизация
T1
T2
t2_ready.
set_value
start
return
return
output
ready.
set_value
t1_ready.
set_value
126
128. Хранение результата для future
Деструктор объекта future ведёт себя иногда так, как
будто он выполняет неявный join, а в некоторых случае -
как будто выполняет неявный detach.
128
Вызываемый
поток
Вызывающий
поток
future std::promise
Где хранится результат вызывающего потока?
Вызывающий поток может завершиться до того, как
вызываемый выполнит fut.get(), и результат не может
храниться в объекте std::promise вызываемого потока.
Объект future не может быть хранилищем для результата,
т.к. он может быть скопирован в объекты shared_future,
после чего возникает вопрос, какая из копий соответствует
результату?
129. Два варианта поведения деструктора future
129
Вызываемый
поток
Вызывающий
поток
future std::promise
Результат
вызывающего
Поведение деструктора future зависит от разделяемого
состояния (shared state):
▪ Деструктор последнего объекта future,
указывающего на разделяемое состояние (shared
state) для какой-то асинхронной задачи, блокируется
до завершения выполнения этой задачи, т.е. выполняет
“join”.
▪ Деструкторы всех других объектов future просто
уничтожают объект future. Это аналогично вызову
detach для потока.
shared state
130. Два варианта поведения деструктора future
130
▪ Деструктор последнего объекта future, указывающего
на разделяемое состояние, выполняет “join”, если:
▫ объект указывает на разделяемое состояние,
созданное std::async
▫ задача, породившая future, была запущена
асинхронно
▫ future - это последний объект future, указывающий
на разделяемое состояние
Зачем это нужно?
▪ Чтобы избежать неявного вызова detach для потока, в
котором выполняется задача.
▪ Срабатывание деструктора не должно приводить к
завершению программы (попытка компромиса)
131. Два варианта поведения деструктора future
131
// Деструктор futs может блокироваться
std::vector<std::future<void>> futs;
// Объект может блокироваться при уничтожении
class Widget {
private:
std::shared_future<double> fut;
};
132. Два варианта поведения деструктора future - пример
132
auto fut1 = std::async(std::launch::async, [] {
std::this_thread::sleep_for(1s);
std::cout << "1st task finishedn";
});
auto fut2 = std::async(std::launch::async, [](auto fut2) {
return "2nd task finishedn";
}, std::move(fut1));
std::cout << fut2.get();
137. Неблокирующие будущие результаты (then)
auto func1() {
std::cout << "begin thinking over the answer...n";
std::this_thread::sleep_for(dur3);
return 40;
}
auto func2(int x) {
std::cout << "continue thinking over the answer...n";
std::this_thread::sleep_for(dur1);
return x + 2;
}
auto func3(int x) {
std::cout << "still thinking...n";
std::this_thread::sleep_for(dur2);
return "number " + std::to_string(x);
}
void do_some_stuff() { std::cout << "do some useful stuff"; }
void do_some_other_stuff() { std::cout << "do other stuff"; }137
138. Неблокирующие будущие результаты (then)
int main() {
auto f1 = std::async(func1);
auto f2 = std::async(func2, f1.get());
auto f3 = std::async(func3, f2.get());
std::cout << "waiting for the answer...n";
do_some_stuff();
std::cout << "answer: " << f3.get() << std::endl;
do_some_other_stuff();
138
139. Неблокирующие будущие результаты (then)
int main() {
auto f1 = std::async(func1);
auto f2 = std::async(func2, f1.get());
auto f3 = std::async(func3, f2.get());
std::cout << "waiting for the answer...n";
do_some_stuff();
std::cout << "answer: " << f3.get() << std::endl;
do_some_other_stuff();
Каждый раз после получения результата выполняется
создание новой асинхронной задачи.
Поток может быть заблокирован при вызове get() для
ожидания результата.
139
140. Неблокирующие будущие результаты (then)
$ ./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
140
141. Неблокирующие будущие результаты (then)
#define BOOST_THREAD_PROVIDES_FUTURE
#define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION
#include <boost/thread/future.hpp>
int main() {
auto f = boost::async([](){
return func1();
});
do_some_stuff();
f.then([](auto f){
std::cout << "answer: " << f.get() << std::endl;
});
do_some_other_stuff();
141
вызывающий поток блокируется
142. Неблокирующие будущие результаты (then)
#define BOOST_THREAD_PROVIDES_FUTURE
#define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION
#include <boost/thread/future.hpp>
int main() {
auto f = boost::async([](){
return func1();
}).then([](auto f){
return func2(f.get());
}).then([](auto f){
return func3(f.get());
});
std::cout << "waiting for the answer...n";
do_some_stuff();
f.then([](auto f){
std::cout << "answer: " << f.get() << std::endl;
});
do_some_other_stuff(); 142
143. Неблокирующие будущие результаты (then)
#define BOOST_THREAD_PROVIDES_FUTURE
#define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION
#include <boost/thread/future.hpp>
int main() {
auto f = boost::async([](){
return func1();
}).then([](auto f){
return func2(f.get());
}).then([](auto f){
return func3(f.get());
});
std::cout << "waiting for the answer...n";
do_some_stuff();
f.then([](auto f){
std::cout << "answer: " << f.get() << std::endl;
});
do_some_other_stuff();
вызывающий поток не блокируется
143
144. Неблокирующие будущие результаты (then)
#define BOOST_THREAD_PROVIDES_FUTURE
#define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION
#include <boost/thread/future.hpp>
int main() {
auto f = boost::async([](){
return func1();
}).then([](auto f){
return func2(f.get());
}).then([](auto f){
return func3(f.get());
});
std::cout << "waiting for the answer...n";
do_some_stuff();
f.then([](auto f){
std::cout << "answer: " << f.get() << std::endl;
}).wait();
do_some_other_stuff();
вызывающий поток блокируется
144
145. Неблокирующие будущие результаты (then)
$ 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
145
146. Неблокирующие будущие результаты (then)
Блокирующие future Неблокирующие future
f2
f3
f1
f
▪ устанавливается явный
порядок выполнения
▪ нет блокировок
▪ поток один
▪ порядок выполнения
неопределён
▪ возможны блокировки
▪ для каждой задачи
создаётся отдельный поток 146
147. Ожидание выполнения всех задач (when_all)
f2
f1
f3
Будущий результат f4 зависит от выполнения всех
будущих результатов f1, f2, f3 и начинает выполняться
после завершения выполнения задач, им соответствующих
(подобно барьерной синхронизации).
f4
147
148. Ожидание выполнения всех задач (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 1n"; }));
task_chunk.emplace_back(boost::async([]()
{ std::cout << "hello from task 2n"; }));
task_chunk.emplace_back(boost::async([]()
{ std::cout << "hello from task 3n"; }));
auto join_task = boost::when_all(task_chunk.begin(),
task_chunk.end());
do_some_stuff();
join_task.wait();
148
149. Ожидание выполнения всех задач (when_all)
std::vector<boost::future<int>> task_chunk;
task_chunk.emplace_back(boost::async(boost::launch::async,
[](){ std::cout << "hello from task 1n"; return 10; }));
task_chunk.emplace_back(boost::async(boost::launch::async,
[](){ std::cout << "hello from task 2n"; return 20; }));
task_chunk.emplace_back(boost::async(boost::launch::async,
[](){ std::cout << "hello from task 3n"; return 12; }));
auto join_task = boost::when_all(task_chunk.begin(),
task_chunk.end())
.then([](auto results){
auto res = 0;
for (auto &elem: results.get())
res += elem.get();
return res;
});
do_some_stuff();
std::cout << "result: " << join_task.get() << std::endl;
join_task
имеет тип
future<
vector<
future<T>>>
149
151. Ожидание выполнения всех задач (when_all)
$ g++ -Wall -pedantic -pthread -lboost_system
-lboost_thread -std=c++14 -O2 prog.cpp -o prog
$ ./prog
hello from task 1
hello from task 3
hello from task 2
do some useful stuff
result: 42
151
152. Ожидание выполнения какой-либо задачи (when_any)
f2
f1
f3
Будущий результат f4 зависит от выполнения одного из
будущих результатов f1, f2, f3 и начинает выполняться
после завершения выполнения хотя бы одной задачи
(подобно синхронизации “эврика”).
f4
152
153. Ожидание выполнения какой-либо задачи (when_any)
std::vector<boost::future<decltype(M_PI)>> task_chunk;
task_chunk.emplace_back(boost::async(boost::launch::async,
[]() { std::this_thread::sleep_for(dur1); return M_PI; }));
task_chunk.emplace_back(boost::async(boost::launch::async,
[]() { std::this_thread::sleep_for(dur2); return M_E; }));
task_chunk.emplace_back(boost::async(boost::launch::async,
[]() { std::this_thread::sleep_for(dur3); return M_LN2; }));
auto join_task = boost::when_any(task_chunk.begin(),
task_chunk.end())
.then([](auto results) {
for (auto &elem: results.get()) {
if (elem.is_ready()) { return elem.get(); }
}
exit(1); // this will never happen
});
do_some_stuff();
std::cout << "result: " << join_task.get() << std::endl;
153
join_task
имеет тип
future<
vector<
future<T>>>
154. Ожидание выполнения какой-либо задачи (when_any)
$ g++ -Wall -pedantic -pthread -lboost_system
-lboost_thread -std=c++14 -O2 prog.cpp -o prog
do some useful stuff
result: 2.71828
do some useful stuff
result: 0.693147
do some useful stuff
result: 3.14159
154
Возможные варианты:
158. Мьютексы и очереди задач
Блокировка мьютекса
▪ потоки блокируются
▪ имеется возможность
дедлока
▪ небольшая
масштабируемость
▪ порядок следования
сообщения в логе отличается
от последовательности
поступления
Очереди задач
▪ потоки не блокируются
▪ отсутствует возможность
дедлока
▪ высокая масштабируемость
▪ порядок следования
сообщения в логе совпадает
с фактическим
158
159. Паттерн: потокобезопасная обёртка над данными
Требования к потокобезопасным “обёрткам”:
1. Сохранение интерфейса
widget w; => w.func("hi folks!");
wrapper<widget> w; => w.func("hi folks!");
2. Универсальность. Заранее может быть неизвестны
методы, которые необходимо обернуть. Некоторые методы
сложно обернуть: конструкторы, операторы, шаблоны и т.д.
3. Поддержка транзакций
account.deposit(Sergey, 1000)
account.withdraw(Ivan, 1000);
log.print("user ", username, "data ");
log.print("time ", logmsg);
Реализация отдельных методов может не обеспечить
необходимую гранулярность.
159
160. Паттерн: обёртка над данными с блокировками
template<typename T>
class wrapper {
private:
T t; // оборачиваемый объект
... // состояние враппера
public:
monitor(T _t): t(_t) { }
template <typename F>
// 1. получаем любую функцию
// 2. подставляем в неё оборачиваемый объект
// 3. выполняем и возвращаем результат
auto operator()(F f) -> decltype(f(t)) {
// работа враппера
auto ret = f(t);
// ...
return ret;
}
}; 160
161. Потокобезопасная обёртка над данными с блокировками
template<typename T>
class monitor {
private:
T t;
std::mutex m;
public:
monitor(T _t): t(_t) { }
template <typename F>
auto operator()(F f) -> decltype(f(t)) {
std::lock_guard<std::mutex> lock(m);
// вызов “объявления” под защитой мьютекса
return f(t);
}
};
161
162. Потокобезопасная обёртка над данными с блокировками
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 << "donen";
162
163. Потокобезопасная обёртка над данными с блокировками
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 163
164. Потокобезопасная обёртка над данными с блокировками
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 << "donen";
}); 164
165. Потокобезопасная обёртка над данными с блокировками
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
165
166. Потокобезопасная обёртка над данными на основе очереди задач
template<typename T> class concurrent {
private: // потокобезопасная очередь
T t;
concurrent_queue<std::function<void()>> q;
bool done = false;
std::thread thd;
public:
concurrent(T t_): t{t_}, thd{[=]{
while (!done) {
(*q.wait_and_pop())(); // дождаться поступления
} // значения, извлечь
} } { } // из очереди и выполнить
~concurrent()
{ q.push([=]{ done = true; });
thd.join(); }
template<typename F> void operator()(F f)
{ q.push([=]{ f(t); }); }
}; 166
167. Потокобезопасная обёртка над данными на основе очереди задач
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 << "donen";
167
168. Потокобезопасная обёртка над данными на основе очереди задач
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 168
169. Потокобезопасная обёртка над данными на основе очереди задач
concurrent<std::string> smon{"start "}; // инициализация
std::vector<std::future<void>> v;
for (auto i = 0; i < 5; i++) { // выполнить асинхр. задачи...
v.emplace_back(std::async(std::launch::deferred, [&, i]{
smon([=](auto &s){ // "объявление" функции
s += "i = " + std::to_string(i);
s += " ";
});
smon([](auto &s){ // "объявление" функции
std::cout << s << std::endl;
});
}));
}
for (auto &f: v) // дождаться завершения
f.wait();
std::cout << "donen";
169
start i = 0
start i = 0 i = 1
start i = 0 i = 1 i = 2
start i = 0 i = 1 i = 2 i = 3
start i = 0 i = 1 i = 2 i = 3 i = 4
done
170. Потокобезопасная обёртка над данными на основе очереди задач
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 << "donen";
}); 170
171. Потокобезопасная обёртка над данными на основе очереди задач
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
171
172. Потокобезопасная обёртка над данными на основе очереди задач
template<typename F>
auto operator()(F f) -> std::future<decltype(f(t))> {
// создаём объект promise (shared_ptr<promise>)
auto p = std::make_shared<std::promise<decltype(f(t))>>();
// получаем из promise объект future
auto ret = p->get_future();
q.push([=]{
// выполняем обещание уже внутри потока
try { p->set_value(f(t)); }
catch (...)
{ p->set_exception(std::current_exception()); }
});
return ret;
}
Данная версия operator() позволяет вернуть значение при вызове
функции:
172
173. Потокобезопасная обёртка над данными на основе очереди задач
template<typename F>
auto operator()(F f) -> std::future<decltype(f(t))> {
auto p = std::make_shared<std::promise<decltype(f(t))>>();
auto ret = p->get_future();
q.push([=]{
try { set_value(*p, f, t); }
catch (...)
{ p->set_exception(std::current_exception()); }
});
return ret;
}
template<typename Fut, typename F, typename T1>
void set_value(std::promise<Fut>& p, F& f, T1& t)
{ p.set_value(f(t)); }
template<typename F, typename T1>
void set_value(std::promise<void>& p, F& f, T1& t)
{ f(t); p.set_value(); }
173
174. Потокобезопасная обёртка над данными на основе очереди задач
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 += "donen";
std::cout << s;
return s;
});
std::cout << "return value: " << f.get() << std::endl; 174
175. Мьютексы и очереди задач
Блокировка мьютекса
▪ потоки блокируются
▪ имеется возможность
дедлока
▪ небольшая
масштабируемость
▪ порядок следования
сообщения в логе отличается
от последовательности
поступления
Очереди задач
▪ потоки не блокируются
▪ отсутствует возможность
дедлока
▪ высокая масштабируемость
▪ порядок следования
сообщения в логе совпадает
с фактическим
175
176. Потокобезопасная обёртка на основе очереди задач - применение
class backgrounder {
public:
future<bool> save(std::string file) {
c([=](data& d) {
... // каждая функция - в отдельной транзакции
});
}
future<size_t> print(some& stuff) {
c([=, &stuff](data& d) {
... // атомарный неделимый вывод
});
}
private:
struct data { /* ... */ } // данные
concurrent<data> c; // обёртка для потокобезопасного
}; // выполнения операций с данными
176