ПВТ - весна 2015 - Лекция 6. Разработка параллельных...
TRANSCRIPT
Лекция 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