Лекция 6. Разработка
параллельных структур данных
на основе блокировок
Пазников Алексей Александрович
Кафедра вычислительных систем СибГУТИ
Сайт курса: http://cpct.sibsutis.ru/~apaznikov/teaching/
Q/A: https://piazza.com/sibsutis.ru/spring2015/pct2015spring
Параллельные вычислительные технологии
Весна 2015 (Parallel Computing Technologies, PCT 15)
Цель разработки параллельных структур данных
▪ Обеспечить параллельный доступ
▪ Обеспечить безопасность доступа
▪ Минимизировать взаимные исключения
▪ Минимизировать сериализацию
2
Цель разработки параллельных структур данных
Задачи проектирования структур данных с блокировками:
▪ Ни один поток не может увидеть состояние, в котором
инварианты нарушены
▪ Предотвратить состояние гонки
▪ Предусмотреть возникновение исключений
▪ Минимизировать возможность взаимоблокировок
Средства достижения:
▪ ограничить область действия блокировок
▪ защитить разные части структуры разными
мьютексами
▪ обеспечить разный уровень защиты
▪ изменить структуру данных для расширения
возможностей распраллеливания 3
Цель разработки параллельных структур данных
Задачи проектирования структур данных с блокировками:
▪ Ни один поток не может увидеть состояние, в котором
инварианты нарушены
▪ Предотвратить состояние гонки
▪ Предусмотреть возникновение исключений
▪ Минимизировать возможность взаимоблокировок
Средства достижения:
▪ ограничить область действия блокировок
▪ защитить разные части структуры разными
мьютексами
▪ обеспечить разный уровень защиты
▪ изменить структуру данных для расширения
возможностей распраллеливания 4
▪ Инвариант - это состояние структуры, которое должно
быть неизменно при любом обращении к структуре (перед
любой операцией и после каждой операции)
Потокобезопасный стек - потенциальные проблемы
Потенциальные проблемы безопасности реализации
потокобезопасных структур:
1. Гонки данных
2. Взаимные блокировки
3. Безопасность относительно исключений
4. Сериализация
5. Голодание
6. Инверсия приоритетов
7. ...
5
Потокобезопасный стек
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));
}
Защита
данных
6
Потокобезопасный стек
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();
}
};
7
Потокобезопасный стек - тестовая программа
threadsafe_stack<int> stack;
void pusher(unsigned nelems) {
for (auto 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();
} 8
Потокобезопасный стек - безопасность исключений
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
9
Версия 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
[невозвратная] модификация контейнера
[невозвратная] модификация контейнера
10
Потокобезопасный стек - взаимоблокировки
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
?
11
Потокобезопасный стек - взаимоблокировки
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
?
12
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();
}
Потокобезопасный стек - тестовая программа
Недостатки реализации:
▪ Сериализация потоков приводит к снижению
производительности: потоки простаивают и не
совершают полезной работы
▪ Нет средств, позволяющих ожидать добавления
элемента
13
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;
}
Потокобезопасная очередь с ожиданием
14
Потокобезопасная очередь с ожиданием
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 { /* ... */ }
15
Потокобезопасная очередь - тестовая программа
threadsafe_queue<int> queue;
void pusher(unsigned nelems) {
for (auto i = 0; i < nelems; i++) {
queue.push(i);
}
}
void poper(unsigned nelems) {
for (auto 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()
16
Потокобезопасная очередь с ожиданием
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 { /* ... */ }
Не вызывается
исключение
17
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)
другие потоки не будут
разбужены
18
Потокобезопасная очередь - модифицированная версия
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
Инициализация
объекта теперь
выполняется не под
защитой блокировки
(и это весьма хорошо)
Объект извлекается
напрямую 19
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 не
инициализируется
- исключение не
возбуждается!
20
Потокобезопасная очередь - модифицированная версия
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 не
инициализируется
Недостатки реализации:
▪ Сериализация потоков приводит к снижению
производительности: потоки простаивают и не
совершают полезной работы
21
Очередь с мелкозернистыми блокировками
Head Tail
22
push() pop()
Очередь с мелкозернистыми блокировками
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
23
Очередь с мелкозернистыми блокировками
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;
} }; 24
Очередь с мелкозернистыми блокировками
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
необходимо будет
защищать оба
одновременно 25
Очередь с мелкозернистыми блокировками
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 -
один и тот же объект 26
Очередь с мелкозернистыми блокировками
Head Tail
next next
27
▪ При пустой очереди head->next и tail->next – есть один
и тот же узел.
▪ В pop и push придётся тогда запирать оба мьютекса. :(
Модифицированная версия
Head Tail
Фиктивный узел
▪ При пустой очереди head и tail указывают на фиктивный
узел, а не равны NULL, причём head == tail.
▪ При очереди с одним элементом head->next и tail->next
указывают на разные узлы (причём head->next == tail), в
результате чего гонки не возникает. 28
Пустая очередь
Head Tail
Фиктивный узел
▪ При пустой очереди head и tail указывают на фиктивный
узел, а не равны NULL, причём head == tail.
29
Очередь с одним элементом
Head Tail
Фиктивный узел
▪ При пустой очереди head и tail указывают на фиктивный
узел, а не равны NULL, причём head == tail.
▪ При очереди с одним элементом head->next и tail-
>next указывают на разные узлы (причём head->next ==
tail), в результате чего гонки не возникает. 30
Очередь с мелкозернистыми блокировками
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
указатель на данные
вместо данных
создание первого фиктивного узла в конструкторе
31
Очередь с мелкозернистыми блокировками
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
создание нового
фиктивного узла
записываем в старый
фиктивный узел новое
значение 32
Добавление нового элемента в очередь (push)
tail next
data
33
Добавление нового элемента в очередь (push)
tail next
data
p(new node)
34
Добавление нового элемента в очередь (push)
tail next
data
tail->data =
new_data
p(new node)
35
Добавление нового элемента в очередь (push)
tail next
data
new_
tail
new_tail =
p.get()
next
data
p(new node)
36
Добавление нового элемента в очередь (push)
tail next new_
tail
tail->next =
std::move(p)
data
next
data
37
Добавление нового элемента в очередь (push)
tail next
data
tail =
new_tail
38
Очередь с мелкозернистыми блокировками
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
39
Потокобезопасная очередь с мелкозернистыми блокировками
Head Tail
▪ Функция push обращается только к tail, try_pop -
только к head (и tail на короткое время).
▪ Вместо единого глобального мьютекса можно завести два
отдельных и удерживать блокировки при доступке к head
и tail.
1 2
40
Потокобезопасная очередь с мелкозернистыми блокировками
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
41
Потокобезопасная очередь с мелкозернистыми блокировками
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, поэтому
используется одна блокировка
42
Потокобезопасная очередь с мелкозернистыми блокировками
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
43
Потокобезопасная очередь с мелкозернистыми блокировками
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
44
Потокобезопасная очередь с мелкозернистыми блокировками и с ожиданием
поступления элемента
Особенности реализации:
▪ Освободить мьютекс в push до вызова notify_one,
чтобы разбуженный поток не ждал освобождения
мьютекса.
▪ Проверку условия можно выполнять под защитой
head_mutex, захватывая tail_mutex только для
чтения tail. Предикат выглядит как head !=
get_tail()
▪ Для версии pop, работающей со ссылкой,
необходимо переопределить wait_and_pop(),
чтобы обеспечить безопасность с точки зрения
исключений. Необходимо сначала скопировать
данные из узла, а потом удалять узел из списка.
45
Потокобезопасная очередь с мелкозернистыми блокировками и с ожиданием
поступления элемента - объявление класса
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(); };
46
Потокобезопасная очередь с мелкозернистыми блокировками и с ожиданием
поступления элемента - добавление новых значений
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();
}
47
Потокобезопасная очередь с мелкозернистыми блокировками и с ожиданием
поступления элемента - ожидение и извлечение элемента
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);
}
Модификация списка в результате удаления
головного элемента.
Ожидание появления данных в
очередиВозврат объекта блокировки 48
Потокобезопасная очередь с мелкозернистыми блокировками и с ожиданием
поступления элемента - ожидение и извлечение элемента
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
49
Потокобезопасная очередь с мелкозернистыми блокировками и с ожиданием
поступления элемента - 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();
}
50
Потокобезопасная очередь с мелкозернистыми блокировками и с ожиданием
поступления элемента - 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());
}
};
51

ПВТ - весна 2015 - Лекция 6. Разработка параллельных структур данных на основе блокировок

  • 1.
    Лекция 6. Разработка параллельныхструктур данных на основе блокировок Пазников Алексей Александрович Кафедра вычислительных систем СибГУТИ Сайт курса: http://cpct.sibsutis.ru/~apaznikov/teaching/ Q/A: https://piazza.com/sibsutis.ru/spring2015/pct2015spring Параллельные вычислительные технологии Весна 2015 (Parallel Computing Technologies, PCT 15)
  • 2.
    Цель разработки параллельныхструктур данных ▪ Обеспечить параллельный доступ ▪ Обеспечить безопасность доступа ▪ Минимизировать взаимные исключения ▪ Минимизировать сериализацию 2
  • 3.
    Цель разработки параллельныхструктур данных Задачи проектирования структур данных с блокировками: ▪ Ни один поток не может увидеть состояние, в котором инварианты нарушены ▪ Предотвратить состояние гонки ▪ Предусмотреть возникновение исключений ▪ Минимизировать возможность взаимоблокировок Средства достижения: ▪ ограничить область действия блокировок ▪ защитить разные части структуры разными мьютексами ▪ обеспечить разный уровень защиты ▪ изменить структуру данных для расширения возможностей распраллеливания 3
  • 4.
    Цель разработки параллельныхструктур данных Задачи проектирования структур данных с блокировками: ▪ Ни один поток не может увидеть состояние, в котором инварианты нарушены ▪ Предотвратить состояние гонки ▪ Предусмотреть возникновение исключений ▪ Минимизировать возможность взаимоблокировок Средства достижения: ▪ ограничить область действия блокировок ▪ защитить разные части структуры разными мьютексами ▪ обеспечить разный уровень защиты ▪ изменить структуру данных для расширения возможностей распраллеливания 4 ▪ Инвариант - это состояние структуры, которое должно быть неизменно при любом обращении к структуре (перед любой операцией и после каждой операции)
  • 5.
    Потокобезопасный стек -потенциальные проблемы Потенциальные проблемы безопасности реализации потокобезопасных структур: 1. Гонки данных 2. Взаимные блокировки 3. Безопасность относительно исключений 4. Сериализация 5. Голодание 6. Инверсия приоритетов 7. ... 5
  • 6.
    Потокобезопасный стек 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)); } Защита данных 6
  • 7.
    Потокобезопасный стек 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(); } }; 7
  • 8.
    Потокобезопасный стек -тестовая программа threadsafe_stack<int> stack; void pusher(unsigned nelems) { for (auto 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(); } 8
  • 9.
    Потокобезопасный стек -безопасность исключений 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 9
  • 10.
    Версия 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 [невозвратная] модификация контейнера [невозвратная] модификация контейнера 10
  • 11.
    Потокобезопасный стек -взаимоблокировки 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 ? 11
  • 12.
    Потокобезопасный стек -взаимоблокировки 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 ? 12
  • 13.
    threadsafe_stack<int> stack; void pusher(unsignednelems) { 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(); } Потокобезопасный стек - тестовая программа Недостатки реализации: ▪ Сериализация потоков приводит к снижению производительности: потоки простаивают и не совершают полезной работы ▪ Нет средств, позволяющих ожидать добавления элемента 13
  • 14.
    template<typename T> classthreadsafe_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; } Потокобезопасная очередь с ожиданием 14
  • 15.
    Потокобезопасная очередь сожиданием 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 { /* ... */ } 15
  • 16.
    Потокобезопасная очередь -тестовая программа threadsafe_queue<int> queue; void pusher(unsigned nelems) { for (auto i = 0; i < nelems; i++) { queue.push(i); } } void poper(unsigned nelems) { for (auto 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() 16
  • 17.
    Потокобезопасная очередь сожиданием 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 { /* ... */ } Не вызывается исключение 17
  • 18.
    template<typename T> classthreadsafe_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) другие потоки не будут разбужены 18
  • 19.
    Потокобезопасная очередь -модифицированная версия 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 Инициализация объекта теперь выполняется не под защитой блокировки (и это весьма хорошо) Объект извлекается напрямую 19
  • 20.
    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 не инициализируется - исключение не возбуждается! 20
  • 21.
    Потокобезопасная очередь -модифицированная версия 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 не инициализируется Недостатки реализации: ▪ Сериализация потоков приводит к снижению производительности: потоки простаивают и не совершают полезной работы 21
  • 22.
    Очередь с мелкозернистымиблокировками Head Tail 22 push() pop()
  • 23.
    Очередь с мелкозернистымиблокировками 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 23
  • 24.
    Очередь с мелкозернистымиблокировками 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; } }; 24
  • 25.
    Очередь с мелкозернистымиблокировками 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 необходимо будет защищать оба одновременно 25
  • 26.
    Очередь с мелкозернистымиблокировками 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 - один и тот же объект 26
  • 27.
    Очередь с мелкозернистымиблокировками Head Tail next next 27 ▪ При пустой очереди head->next и tail->next – есть один и тот же узел. ▪ В pop и push придётся тогда запирать оба мьютекса. :(
  • 28.
    Модифицированная версия Head Tail Фиктивныйузел ▪ При пустой очереди head и tail указывают на фиктивный узел, а не равны NULL, причём head == tail. ▪ При очереди с одним элементом head->next и tail->next указывают на разные узлы (причём head->next == tail), в результате чего гонки не возникает. 28
  • 29.
    Пустая очередь Head Tail Фиктивныйузел ▪ При пустой очереди head и tail указывают на фиктивный узел, а не равны NULL, причём head == tail. 29
  • 30.
    Очередь с однимэлементом Head Tail Фиктивный узел ▪ При пустой очереди head и tail указывают на фиктивный узел, а не равны NULL, причём head == tail. ▪ При очереди с одним элементом head->next и tail- >next указывают на разные узлы (причём head->next == tail), в результате чего гонки не возникает. 30
  • 31.
    Очередь с мелкозернистымиблокировками 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 указатель на данные вместо данных создание первого фиктивного узла в конструкторе 31
  • 32.
    Очередь с мелкозернистымиблокировками 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 создание нового фиктивного узла записываем в старый фиктивный узел новое значение 32
  • 33.
    Добавление нового элементав очередь (push) tail next data 33
  • 34.
    Добавление нового элементав очередь (push) tail next data p(new node) 34
  • 35.
    Добавление нового элементав очередь (push) tail next data tail->data = new_data p(new node) 35
  • 36.
    Добавление нового элементав очередь (push) tail next data new_ tail new_tail = p.get() next data p(new node) 36
  • 37.
    Добавление нового элементав очередь (push) tail next new_ tail tail->next = std::move(p) data next data 37
  • 38.
    Добавление нового элементав очередь (push) tail next data tail = new_tail 38
  • 39.
    Очередь с мелкозернистымиблокировками 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 39
  • 40.
    Потокобезопасная очередь смелкозернистыми блокировками Head Tail ▪ Функция push обращается только к tail, try_pop - только к head (и tail на короткое время). ▪ Вместо единого глобального мьютекса можно завести два отдельных и удерживать блокировки при доступке к head и tail. 1 2 40
  • 41.
    Потокобезопасная очередь смелкозернистыми блокировками 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 41
  • 42.
    Потокобезопасная очередь смелкозернистыми блокировками 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, поэтому используется одна блокировка 42
  • 43.
    Потокобезопасная очередь смелкозернистыми блокировками 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 43
  • 44.
    Потокобезопасная очередь смелкозернистыми блокировками 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 44
  • 45.
    Потокобезопасная очередь смелкозернистыми блокировками и с ожиданием поступления элемента Особенности реализации: ▪ Освободить мьютекс в push до вызова notify_one, чтобы разбуженный поток не ждал освобождения мьютекса. ▪ Проверку условия можно выполнять под защитой head_mutex, захватывая tail_mutex только для чтения tail. Предикат выглядит как head != get_tail() ▪ Для версии pop, работающей со ссылкой, необходимо переопределить wait_and_pop(), чтобы обеспечить безопасность с точки зрения исключений. Необходимо сначала скопировать данные из узла, а потом удалять узел из списка. 45
  • 46.
    Потокобезопасная очередь смелкозернистыми блокировками и с ожиданием поступления элемента - объявление класса 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(); }; 46
  • 47.
    Потокобезопасная очередь смелкозернистыми блокировками и с ожиданием поступления элемента - добавление новых значений 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(); } 47
  • 48.
    Потокобезопасная очередь смелкозернистыми блокировками и с ожиданием поступления элемента - ожидение и извлечение элемента 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); } Модификация списка в результате удаления головного элемента. Ожидание появления данных в очередиВозврат объекта блокировки 48
  • 49.
    Потокобезопасная очередь смелкозернистыми блокировками и с ожиданием поступления элемента - ожидение и извлечение элемента 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 49
  • 50.
    Потокобезопасная очередь смелкозернистыми блокировками и с ожиданием поступления элемента - 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(); } 50
  • 51.
    Потокобезопасная очередь смелкозернистыми блокировками и с ожиданием поступления элемента - 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()); } }; 51