ЛЕКЦИЯ 6. Разработка параллельных структур данных на основе блокировок
Курс "Параллельные вычислительные технологии" (ПВТ), весна 2015
Сибирский государственный университет телекоммуникаций и информатики
Пазников Алексей Александрович
к.т.н., доцент кафедры вычислительных систем СибГУТИ
http://cpct.sibsutis.ru/~apaznikov
ПВТ - весна 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
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
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
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
45. Потокобезопасная очередь с мелкозернистыми блокировками и с ожиданием
поступления элемента
Особенности реализации:
▪ Освободить мьютекс в push до вызова notify_one,
чтобы разбуженный поток не ждал освобождения
мьютекса.
▪ Проверку условия можно выполнять под защитой
head_mutex, захватывая tail_mutex только для
чтения tail. Предикат выглядит как head !=
get_tail()
▪ Для версии pop, работающей со ссылкой,
необходимо переопределить wait_and_pop(),
чтобы обеспечить безопасность с точки зрения
исключений. Необходимо сначала скопировать
данные из узла, а потом удалять узел из списка.
45