SlideShare a Scribd company logo
Лекция 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

More Related Content

PDF
ПВТ - осень 2014 - Лекция 6 - Атомарные операции. Внеочередное выполнение инс...
Alexey Paznikov
 
PDF
ПВТ - весна 2015 - Лекция 5. Многопоточное программирование в С++. Синхрониза...
Alexey Paznikov
 
PDF
ПВТ - осень 2014 - Лекция 5 - Многопоточное программирование в языке С++. Р...
Alexey Paznikov
 
PDF
ПВТ - весна 2015 - Лекция 3. Реентерабельность. Сигналы. Локальные данные пот...
Alexey Paznikov
 
PDF
ПВТ - весна 2015 - Лекция 8. Многопоточное программирование без использования...
Alexey Paznikov
 
PDF
ПВТ - осень 2014 - Лекция 7. Многопоточное программирование без блокировок. М...
Alexey Paznikov
 
PDF
Кулагин И.И., Пазников А.А., Курносов М.Г. Оптимизация информационных обменов...
Alexey Paznikov
 
PDF
ПВТ - весна 2015 - Лекция 2. POSIX Threads. Основные понятия многопоточного п...
Alexey Paznikov
 
ПВТ - осень 2014 - Лекция 6 - Атомарные операции. Внеочередное выполнение инс...
Alexey Paznikov
 
ПВТ - весна 2015 - Лекция 5. Многопоточное программирование в С++. Синхрониза...
Alexey Paznikov
 
ПВТ - осень 2014 - Лекция 5 - Многопоточное программирование в языке С++. Р...
Alexey Paznikov
 
ПВТ - весна 2015 - Лекция 3. Реентерабельность. Сигналы. Локальные данные пот...
Alexey Paznikov
 
ПВТ - весна 2015 - Лекция 8. Многопоточное программирование без использования...
Alexey Paznikov
 
ПВТ - осень 2014 - Лекция 7. Многопоточное программирование без блокировок. М...
Alexey Paznikov
 
Кулагин И.И., Пазников А.А., Курносов М.Г. Оптимизация информационных обменов...
Alexey Paznikov
 
ПВТ - весна 2015 - Лекция 2. POSIX Threads. Основные понятия многопоточного п...
Alexey Paznikov
 

What's hot (20)

PDF
ПВТ - осень 2014 - Лекция 4 - Стандарт POSIX Threads. Реентерабельность. Сигн...
Alexey Paznikov
 
PDF
ПВТ - весна 2015 - Лекция 4. Шаблоны многопоточного программирования
Alexey Paznikov
 
PDF
ПВТ - весна 2015 - Лекция 0. Описание курса
Alexey Paznikov
 
PDF
ПВТ - весна 2015 - Лекция 7. Модель памяти С++. Внеочередное выполнение инстр...
Alexey Paznikov
 
PDF
Parallel STL
Evgeny Krutko
 
PDF
Модель памяти C++ - Андрей Янковский, Яндекс
Yandex
 
PDF
Дмитрий Кашицын, Троллейбус из буханки: алиасинг и векторизация в LLVM
Sergey Platonov
 
PDF
Юрий Ефимочев, Компилируемые в реальном времени DSL для С++
Sergey Platonov
 
PDF
Использование юнит-тестов для повышения качества разработки
victor-yastrebov
 
PDF
Дракон в мешке: от LLVM к C++ и проблемам неопределенного поведения
Platonov Sergey
 
PPTX
Григорий Демченко, Универсальный адаптер
Sergey Platonov
 
PPTX
Некоторые паттерны реализации полиморфного поведения в C++ – Дмитрий Леванов,...
Yandex
 
PPTX
Григорий Демченко, Асинхронность и неблокирующая синхронизация
Sergey Platonov
 
PDF
Догнать и перегнать boost::lexical_cast
Roman Orlov
 
PDF
Полухин Антон, Как делать не надо: C++ велосипедостроение для профессионалов
Sergey Platonov
 
PDF
Лекция 8. Intel Threading Building Blocks
Mikhail Kurnosov
 
PDF
Лекция 8: Многопоточное программирование: Intel Threading Building Blocks
Mikhail Kurnosov
 
PDF
Для чего мы делали свой акторный фреймворк и что из этого вышло?
Yauheni Akhotnikau
 
PDF
Лекция 2. Коллективные операции в MPI. Параллельные алгоритмы случайного блуж...
Alexey Paznikov
 
PDF
Антон Полухин, Немного о Boost
Sergey Platonov
 
ПВТ - осень 2014 - Лекция 4 - Стандарт POSIX Threads. Реентерабельность. Сигн...
Alexey Paznikov
 
ПВТ - весна 2015 - Лекция 4. Шаблоны многопоточного программирования
Alexey Paznikov
 
ПВТ - весна 2015 - Лекция 0. Описание курса
Alexey Paznikov
 
ПВТ - весна 2015 - Лекция 7. Модель памяти С++. Внеочередное выполнение инстр...
Alexey Paznikov
 
Parallel STL
Evgeny Krutko
 
Модель памяти C++ - Андрей Янковский, Яндекс
Yandex
 
Дмитрий Кашицын, Троллейбус из буханки: алиасинг и векторизация в LLVM
Sergey Platonov
 
Юрий Ефимочев, Компилируемые в реальном времени DSL для С++
Sergey Platonov
 
Использование юнит-тестов для повышения качества разработки
victor-yastrebov
 
Дракон в мешке: от LLVM к C++ и проблемам неопределенного поведения
Platonov Sergey
 
Григорий Демченко, Универсальный адаптер
Sergey Platonov
 
Некоторые паттерны реализации полиморфного поведения в C++ – Дмитрий Леванов,...
Yandex
 
Григорий Демченко, Асинхронность и неблокирующая синхронизация
Sergey Platonov
 
Догнать и перегнать boost::lexical_cast
Roman Orlov
 
Полухин Антон, Как делать не надо: C++ велосипедостроение для профессионалов
Sergey Platonov
 
Лекция 8. Intel Threading Building Blocks
Mikhail Kurnosov
 
Лекция 8: Многопоточное программирование: Intel Threading Building Blocks
Mikhail Kurnosov
 
Для чего мы делали свой акторный фреймворк и что из этого вышло?
Yauheni Akhotnikau
 
Лекция 2. Коллективные операции в MPI. Параллельные алгоритмы случайного блуж...
Alexey Paznikov
 
Антон Полухин, Немного о Boost
Sergey Platonov
 
Ad

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

PDF
Lab5
ssuser568529
 
PDF
2012 03 14_parallel_programming_lecture05
Computer Science Club
 
PDF
Антон Потапов — С++ контейнеры и многопоточность: вместе или врозь?
Yandex
 
PPT
Как построить высокопроизводительный Front-end сервер (Александр Крижановский)
Ontico
 
PDF
Введение в разработку многопоточных приложений
CUSTIS
 
PDF
Конкурентные ассоциативные контейнеры
corehard_by
 
PDF
1 встреча — Параллельное программирование (А. Свириденков)
Smolensk Computer Science Club
 
PDF
Помоги ближнему, или Как потоки помогают друг другу
Alexey Fyodorov
 
PDF
Помоги ближнему, или Как потоки помогают друг другу
CEE-SEC(R)
 
PPTX
Асинхронность и сопрограммы
Platonov Sergey
 
PDF
Что особенного в СУБД для данных в оперативной памяти / Константин Осипов (Ta...
Ontico
 
PDF
Этюды о буферизации: асинхронные оповещения, репликация обновлений, объединен...
corehard_by
 
PDF
Оптимизация программ для современных процессоров и Linux, Александр Крижановс...
Ontico
 
PPTX
Операционные системы 2015, лекция № 5
Aleksey Bragin
 
PPTX
Исключительная модель памяти. Алексей Ткаченко ➠ CoreHard Autumn 2019
corehard_by
 
PDF
20140310 parallel programming_kalishenko_lecture03-04
Computer Science Club
 
PPTX
Asynchrony and coroutines
corehard_by
 
PDF
Разработка высокопроизводительных серверных приложений для Linux/Unix (Алекса...
Ontico
 
PDF
Atomics, CAS and Nonblocking algorithms
Alexey Fyodorov
 
PDF
How threads help each other
Alexey Fyodorov
 
2012 03 14_parallel_programming_lecture05
Computer Science Club
 
Антон Потапов — С++ контейнеры и многопоточность: вместе или врозь?
Yandex
 
Как построить высокопроизводительный Front-end сервер (Александр Крижановский)
Ontico
 
Введение в разработку многопоточных приложений
CUSTIS
 
Конкурентные ассоциативные контейнеры
corehard_by
 
1 встреча — Параллельное программирование (А. Свириденков)
Smolensk Computer Science Club
 
Помоги ближнему, или Как потоки помогают друг другу
Alexey Fyodorov
 
Помоги ближнему, или Как потоки помогают друг другу
CEE-SEC(R)
 
Асинхронность и сопрограммы
Platonov Sergey
 
Что особенного в СУБД для данных в оперативной памяти / Константин Осипов (Ta...
Ontico
 
Этюды о буферизации: асинхронные оповещения, репликация обновлений, объединен...
corehard_by
 
Оптимизация программ для современных процессоров и Linux, Александр Крижановс...
Ontico
 
Операционные системы 2015, лекция № 5
Aleksey Bragin
 
Исключительная модель памяти. Алексей Ткаченко ➠ CoreHard Autumn 2019
corehard_by
 
20140310 parallel programming_kalishenko_lecture03-04
Computer Science Club
 
Asynchrony and coroutines
corehard_by
 
Разработка высокопроизводительных серверных приложений для Linux/Unix (Алекса...
Ontico
 
Atomics, CAS and Nonblocking algorithms
Alexey Fyodorov
 
How threads help each other
Alexey Fyodorov
 
Ad

More from Alexey Paznikov (17)

PDF
Лекция 6. Параллельная сортировка. Алгоритмы комбинаторного поиска. Параллель...
Alexey Paznikov
 
PDF
Лекция 5. Метод конечных разностей (параллельные алгоритмы в стандарте MPI)
Alexey Paznikov
 
PDF
Лекция 4. Производные типы данных в стандарте MPI
Alexey Paznikov
 
PDF
Лекция 3. Виртуальные топологии в MPI. Параллельные алгоритмы в стандарте MPI...
Alexey Paznikov
 
PDF
Лекция 1. Основные понятия стандарта MPI. Дифференцированные обмены
Alexey Paznikov
 
PDF
ПВТ - весна 2015 - Лекция 1. Актуальность параллельных вычислений. Анализ пар...
Alexey Paznikov
 
PDF
ПВТ - осень 2014 - Лекция 3 - Стандарт POSIX Threads
Alexey Paznikov
 
PDF
ПВТ - осень 2014 - Лекция 2 - Архитектура вычислительных систем с общей памятью
Alexey Paznikov
 
PDF
ПВТ - осень 2014 - лекция 1 - Введение в параллельные вычисления
Alexey Paznikov
 
PDF
ПВТ - осень 2014 - лекция 1а - Описание курса
Alexey Paznikov
 
PDF
Анализ эффективности выполнения алгоритма параллельной редукции в языке Cray ...
Alexey Paznikov
 
PPTX
ТФРВС - весна 2014 - лекция 11
Alexey Paznikov
 
PPTX
ТФРВС - весна 2014 - лекция 10
Alexey Paznikov
 
PPTX
ТФРВС - весна 2014 - лекция 9
Alexey Paznikov
 
PPTX
ТФРВС - весна 2014 - лекция 8
Alexey Paznikov
 
PPTX
ТФРВС - весна 2014 - лекция 7
Alexey Paznikov
 
PPTX
ТФРВС - весна 2014 - лекция 6
Alexey Paznikov
 
Лекция 6. Параллельная сортировка. Алгоритмы комбинаторного поиска. Параллель...
Alexey Paznikov
 
Лекция 5. Метод конечных разностей (параллельные алгоритмы в стандарте MPI)
Alexey Paznikov
 
Лекция 4. Производные типы данных в стандарте MPI
Alexey Paznikov
 
Лекция 3. Виртуальные топологии в MPI. Параллельные алгоритмы в стандарте MPI...
Alexey Paznikov
 
Лекция 1. Основные понятия стандарта MPI. Дифференцированные обмены
Alexey Paznikov
 
ПВТ - весна 2015 - Лекция 1. Актуальность параллельных вычислений. Анализ пар...
Alexey Paznikov
 
ПВТ - осень 2014 - Лекция 3 - Стандарт POSIX Threads
Alexey Paznikov
 
ПВТ - осень 2014 - Лекция 2 - Архитектура вычислительных систем с общей памятью
Alexey Paznikov
 
ПВТ - осень 2014 - лекция 1 - Введение в параллельные вычисления
Alexey Paznikov
 
ПВТ - осень 2014 - лекция 1а - Описание курса
Alexey Paznikov
 
Анализ эффективности выполнения алгоритма параллельной редукции в языке Cray ...
Alexey Paznikov
 
ТФРВС - весна 2014 - лекция 11
Alexey Paznikov
 
ТФРВС - весна 2014 - лекция 10
Alexey Paznikov
 
ТФРВС - весна 2014 - лекция 9
Alexey Paznikov
 
ТФРВС - весна 2014 - лекция 8
Alexey Paznikov
 
ТФРВС - весна 2014 - лекция 7
Alexey Paznikov
 
ТФРВС - весна 2014 - лекция 6
Alexey Paznikov
 

ПВТ - весна 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(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
  • 14. 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
  • 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> 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
  • 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