ПВТ - осень 2014 - Лекция 7. Многопоточное...

Post on 23-Jul-2015

644 Views

Category:

Education

2 Downloads

Preview:

Click to see full reader

TRANSCRIPT

Лекция 7. Многопоточное программирование без блокировок. Модель потребитель-производитель. Потокобезопасный стек

Пазников Алексей АлександровичКафедра вычислительных систем СибГУТИ

Сайт курса: http://cpct.sibsutis.ru/~apaznikov/teaching/Вопросы: https://piazza.com/sibsutis.ru/fall2014/pct14/home

Параллельные вычислительные технологииОсень 2014 (Parallel Computing Technologies, PCT 14)

Программирование без блокировок

2

Если вы думаете, что программирование без блокировок это просто, значит или вы - один из тех 50, которые умеют

это делать, или же используете атомарные инструкции недостаточно аккуратно.

Герб Саттер

Цели многопоточного программирования без блокировок

3

Однопоточная программа

Многопоточная программа с блокировками

Многопоточная программа без блокировок

Цели многопоточного программирования без блокировок

▪ Повышение масштабируемости путём сокращения блокировок и ожиданий в алгоритмах и структурах данных.

▪ Решение проблем, связанных с блокировками:

▫ Гонки: нужно не забыть заблокировать, причём именно нужный участок кода

▫ Дедлоки: необходимо запирать в нужном порядке различные потоки

▫ Сложность выбора критической секции (простота или масштабируемость)

▫ Голоданеие, инверсия приоритетов и др.4

Виды алгоритмов, свободных от блокировок

▪ Свободные от ожиданий (wait-free). “Никто никогда не ждёт”. Каждая операция завершается за N шагов без каких-либо условий. Гарантии:

▫ максимум пропускной способности системы

▫ отсутствие голодания

▪ Свободные от блокировок (lock-free). “Всегда какой-то из потоков работает”. Каждый шаг приближает итоговое решение. Гарантии:

▫ максимум пропускной способности системы

▫ отсутствие голодания (один поток может постоянно ожидать)

▪ Свободные от остановок (obstruction-free). “Поток работает, если нет конфликтов”. За ограниченное число шагов один поток, при условии, что другие потоки остановлены, достигает результата.

▫ Все потоки не блокируются из-за проблем (задержек, ошибок) с другими потоками.

▫ Не гарантируется прогресс, если одновременно работают два или больше потоков. 5

Реализация спинлока на основе атомарного флага

class spinlock_mutex { std::atomic_flag flag;

public: spinlock_mutex(): flag{ATOMIC_FLAG_INIT} { }

void lock() { while (flag.test_and_set( std::memory_order_acquire)); }

void unlock() { flag.clear(std::memory_order_release); }};

n.b. Можно использовать с lock_guard и unique_guard! 6

Реализация модели потребитель-производитель без блокировок

7

Производитель-потребитель на основе блокировок

8

Производитель

Потребитель

Потребитель

Потребитель

Производитель-потребитель на основе блокировок

void producer() {

while (ThereAreTasks()) { task = BuildNewTask(); std::unique_lock<std::mutex> lock{mut}; queue.push(task); lock.unlock(); cv.notify(); }

std::unique_lock<std::mutex> lock{mut}; queue.push(done); // добавить признак окончания lock.unlock(); cv.notify();}

9

Производитель-потребитель на основе блокировок

void consumer() { task = nullptr; while (task != done) { std::unique_lock<std::mutex> lock{mut}; cv.wait(mut, []{ return queue.empty(); }); task = queue.first(); if (task != done) queue.pop(); }

if (task != done) DoWork(task);}

10

Производитель-потребитель без блокировок

11

is empty?

empty

is empty?

is empty?

empty

is empty?

is empty?

empty

is empty?

is empty?

empty

is empty?

Производитель-потребитель без блокировок

12

put out

is empty?

is empty?

empty

put out

full

is empty?

empty full

is empty?

is empty?

is empty?

Производитель-потребитель без блокировок

13

is empty?

is empty?

empty

put in

full

is empty?

empty full

is empty?

process process

is empty?

Производитель-потребитель без блокировок

14

is empty?

is empty?

put in

full

is empty?

empty full

is empty?

process process

full

is empty?

Производитель-потребитель без блокировок

15

is empty?

is empty?

full

is empty?

full

is empty?

is empty?

empty fulldone done

put indone flag

put output out

Производитель-потребитель без блокировок

16

is empty?

full

is empty?

full

is empty?

done donefull

done

put out

empty

is empty?

put indone flag

Производитель-потребитель без блокировок

17

full

is empty?

full

is empty?

done donefull

done

is empty?

is empty?

donefull

put out

Производитель-потребитель без блокировок

18

full

is empty?

full

is empty?

done donefull

done

is empty?

is empty?

donefull

Производитель-потребитель без блокировок

19

Empty Task Done

Start

End

Производитель

Потребитель

Производитель

20

curr = 0; // указатель на текущий слотwhile (ThereAreMoreTasks()) { task = AllocateAndBuildNewTask(); while (slot[curr] != null) // если null, то проверить curr = (curr + 1) % numOfConsumers; // следующий слот slot[curr] = task; sem[curr].signal();}

Производитель

21

curr = 0; // указатель на текущий слотwhile (ThereAreMoreTasks()) { task = AllocateAndBuildNewTask(); while (slot[curr] != null) // если null, то проверить curr = (curr + 1) % numOfConsumers; // следующий слот slot[curr] = task; sem[curr].signal();}

// Фаза 2: выставить флаги “done” во всех слотахnumNotified = 0;while (numNotified < numOfConsumers) { while (slot[curr] != null) // если null, то проверить curr = (curr + 1) % numOfConsumers; // следующий slot[curr] = done; // освободить слот sem[curr].signal(); ++numNotified;}

Потребитель

22

task = null;while (task != done)

// Дождаться, когда слот будет полным while ((task = slot[mySlot]) == null) sem[mySlot].wait();

if (task != done) { slot[mySlot] = null; // помечаем слот пустым DoWork(task); // выполняем задачу } // вне критической секции}

Потребитель

23

task = null;while (task != done)

// Дождаться, когда слот будет полным while ((task = slot[mySlot]) == null) sem[mySlot].wait();

if (task != done) { slot[mySlot] = null; // помечаем слот пустым DoWork(task); // выполняем задачу } // вне критической секции}

Как применить модель памяти С++?

Производитель, модель памяти С++

24

curr = 0; while (ThereAreMoreTasks()) { task = AllocateAndBuildNewTask();

while (slot[curr] != null) // acquire null curr = (curr + 1) % numOfConsumers; slot[curr] = task; // release non-null sem[curr].signal();}// Фаза 2: выставить флаги “done” во всех слотахnumNotified = 0;while (numNotified < numOfConsumers) { while (slot[curr] != null) // acquire null curr = (curr + 1) % numOfConsumers; slot[curr] = done; // release done sem[curr].signal(); ++numNotified;}

Птребитель, модель памяти С++

25

task = null;while (task != done)

// Дождаться, когда слот будет полным while ((task = slot[mySlot]) // acquire non-null == null) sem[mySlot].wait();

if (task != done) { slot[mySlot] = null; // release null DoWork(task); } }

Производитель-потребитель, класс алгоритма

26

curr = 0; while (ThereAreMoreTasks()) { task = AllocateAndBuildNewTask();

while (slot[curr] != null) // acquire null curr = (curr + 1) % numOfConsumers; slot[curr] = task; // release non-null sem[curr].signal();}// Фаза 2: выставить флаги “done” во всех слотахnumNotified = 0;while (numNotified < numOfConsumers) { while (slot[curr] != null) // acquire null curr = (curr + 1) % numOfConsumers; slot[curr] = done; // release done sem[curr].signal(); ++numNotified;}

Алгоритм - свободный от ожиданий, свободный от блокировок или свободный от остановок?

Производитель-потребитель, класс алгоритма

27

curr = 0; while (ThereAreMoreTasks()) { task = AllocateAndBuildNewTask();

while (slot[curr] != null) // acquire null curr = (curr + 1) % numOfConsumers; slot[curr] = task; // release non-null sem[curr].signal();}// Фаза 2: выставить флаги “done” во всех слотахnumNotified = 0;while (numNotified < numOfConsumers) { while (slot[curr] != null) // acquire null curr = (curr + 1) % numOfConsumers; slot[curr] = done; // release done sem[curr].signal(); ++numNotified;}

Алгоритм - свободный от ожиданий, свободный от блокировок или свободный от остановок?

Этап 2:Свободная от остановок

Этап 1:Свободный от ожиданий

Производитель-потребитель без блокировок

28

task = null;while (task != done)

// Дождаться, когда слот будет полным while ((task = slot[mySlot]) == null) sem[mySlot].wait();

if (task != done) { slot[mySlot] = null; DoWork(task); } }

можно ли поменять две строки?нужно ли это сделать?

Реализация стека, свободного от блокировок, на основе сборщика мусора

29

Стек, свободный от блокировок

30

T T T T

head

1. Конструктор

2. Деструктор

3. Поиск узла (find)

4. Добавление узла (push)

5. Удаление узла (pop)

Стек, свободный от блокировок

31

template <typename T>class lfstack {public: lfstack(); ~lfstack(); T* find(T data) const; // найти элемент, равный data void push(T data); // добавить элемент в голову

private: struct node { // атомарные операции T data; // не требуются node* next; }; std::atomic<node*> head{nullptr}; // атомарный указатель}; // на голову списка

Конструктор и деструктор

32

template <typename T>lfstack<T>::lfstack() {}

Объект создаётся в одном потоке, поэтому не нужно обеспечивать параллельный доступ. Нельзя использовать стек до тех пор, пока он не будет создан, т.е. до выполнения конструктора, и после того, как он будет уничтожен, т.е. после выполнения деструктора.

template <typename T>lfstack<T>::~lfstack() { auto first = head.load(); while (first) { auto unlinked = first; first = first->next; delete unlinked; }}

Функция push

33

1. Создать новый узел.

2. Записать в его указатель next текущее значение head.

3. Записать в head указатель на новый узел.

void push(T const& data) { auto new_node = new node{data}; // (1) node_node->next = head.load(); // (2) head = new_node; // (3)}

struct node { T data; node* next; node(T const& _data): data{_data} {}};

Функция push

34

T

head

TTT

Начальная стадия

Промежуточная стадия

T

head

TTT

T

Конечная стадия

T

head

TTT

T

Функция push

35

T

head

TTT

Начальная стадия

Промежуточная стадия выполняется двумя потоками

T

head

TTT

T

Первый добавляемый элемент пропал, остался только второй

T

head

TTT

T

T T

Функция push

36

void push(T const& data) { auto new_node = new node{data}; // (1) node_node->next = head.load(); // (2) head = new_node; // (3) while (!head.compare_exchange_weak(new_node->next, new_node)); // (3)}

1. Создать новый узел.

2. Записать в его указатель next текущее значение head.

3. Записать в head указатель на новый узел, при этом с помощью операции сравнить-и-обменять гарантировать то, что head не был модифицирован с момента чтения на шаге 2.

Функция pop (ошибочная)

37

void pop(T& result) { node* old_head = head.load(); head = head->next; result = old_head->data; }

Функция pop

38

T

head

TTT

Начальная стадия

Промежуточная стадия

T

head

TTT

Конечная стадия

T

head

TT

Функция pop

39

T

head

TTT

Начальная стадия

Промежуточная стадия

T

head

TTT

Конечная стадия

T

head

TT

B

A

A B

Функция pop (ошибочная)

40

void pop(T& result) { node* old_head = head.load();

while (!head.compare_exchange_weak(old_head, old_head->next); result = old_head->data; }

Функция pop (ошибочная)

41

void pop(T& result) { node* old_head = head.load();

while (!head.compare_exchange_weak(old_head, old_head->next); result = old_head->data; }

Функция pop (ошибочная)

42

void pop(T& result) { node* old_head = head.load();

while (!head.compare_exchange_weak(old_head, old_head->next); result = old_head->data; }

std::shared_ptr<T> pop(T& result) { node* old_head = head.load(); while (old_head && !head.compare_exchange_weak(old_head, old_head->next)); return old_head ? old_head->data : std::shared_ptr<T>(); }

Функция pop (ошибочная)

43

class lfstack {private: struct node { std::shared_ptr<T> data; node* next; node(T const& _data): data(std::make_shared<T>(_data)) { } };

...

std::shared_ptr<T> pop(T& result) { node* old_head = head.load(); while (old_head && !head.compare_exchange_weak(old_head, old_head->next)); return old_head ? old_head->data : std::shared_ptr<T>(); }

Проблема АВА

44

4

old_head

321

head->nextПоток А выполняет удаление узла

из вершины стека

Проблема АВА

45

4

old_head

321

head->next

43

Поток А выполняет удаление узла из вершины стека

old_head old_head->nextПоток А был вытеснен и другие

потоки удалили два узла из стека

Проблема АВА

46

4

old_head

321

head->next

43

Поток А был вытеснен и другие потоки удалили два узла из стека

Поток А выполняет удаление узла из вершины стека

435

old_head old_head->next

old_head old_head->nextНекий поток добавил новый узел и

аллокатор выделил под него ту же память

Проблема АВА

47

4

old_head

321

head->next

43

Поток А выполняет удаление узла из вершины стека

43

Некий поток добавил новый узел и аллокатор выделил под него ту же память

5

43

Поток А: head.compare_exchange( old_head, old_head->next))

5

old_head old_head->next

old_head old_head->next

old_head old_head->next

Поток А был вытеснен и другие потоки удалили два узла из стека

Проблема АВА

48

4

old_head

321

head->next

43

Поток А выполняет удаление узла из вершины стека

435

43

Некий поток добавил новый узел и аллокатор выделил под него ту же память

Поток А: head.compare_exchange( old_head, old_head->next))

5

old_head old_head->next

old_head old_head->next

old_head old_head->next

Поток А был вытеснен и другие потоки удалили два узла из стека

Решения проблемы АВА

49

1. “Ленивый” сборщик мусора

2. Указатели опасности

3. Счётчик ссылок на элемент

4. Сделать узлы уникальными

5. Вообще не удалять узлы

6. Добавление дополнительных узлов

7. и т.д.

Функция pop (наивная)

50

// количество потоков, выполняющих popstd::atomic<unsigned> threads_in_pop;

std::shared_ptr<T> pop() { threads_in_pop++;

node* old_head = head_load(); while (old_head && !head.compare_exchange_weak(old_head, old_head->next)); std::shared_ptr<T> res; if (old_head) res.swap(old_head->data); // не копировать, // а обменять данные

try_reclaim(old_head); // попробовать освободить // удалённые узлы return res;}

Функция pop (наивная)

51

template<typename T>class lfstack {private: std::atomic<node*> delete_list;

static void delete_nodes(node* nodes); while (nodes) { node* next = nodes->next; delete nodes; nodes = next; } }

Функция try_reclaim освобождения удалённых узлов

52

void try_reclaim(node* old_head) { if (threads_in_pop == 1) { // я единственный в pop?

node* nodes_to_delete = // захватить список delete_list.exchange{nullptr}; // на удаление

if (!--thread_in_pop) // точно единственный? delete_nodes(nodes_to_delete)); // удалить всё! else if (nodes_to_delete) // если в захваченном списке // что-то было // вернуть это в общий список узлов на удаление chain_pending_nodes(nodes_to_delete);

delete old_head; // удаляем хотя бы только что // исключённый узел } else { // удалим узел как-нибудь потом chain_pending_node(old_head); --threads_in_pop; }}

Функция try_reclaim освобождения удалённых узлов

53

// добавляем захваченный список в общий список узлов,// подлежащих удалениюvoid chain_pending_nodes(node* nodes) { node* last = nodes; while (node* const next = last->next) last = next; chain_pending_nodes(nodes, last);}

// добавить список узлов в список узлов на удалениеvoid chain_pending_nodes(node* first, node* last) { last->next = delete_list; while (!delete_list.compare_exchange_weak(last->next, first));}

// добавить узел в список узлов на удалениеvoid chain_pending_node(node* n) { chain_pending_nodes(n, n);}

Функция try_reclaim освобождения удалённых узлов

54

4

head

321

delete_list 0threads_in_pop

4

head

321

delete_list

threads_in_pop 1

5

5A A удаляет узел 1 и

вытесняется в pop() после 1-го чтения threads_in_pop

Функция try_reclaim освобождения удалённых узлов

55

4

head

32

delete_list 2threads_in_pop

43

delete_list

threads_in_pop 2

5

2

С удаляет узел и продолжает работать до момента выхода из pop()

old_head

BB вызывает pop() и

вытесняется после 1-го чтения head

A

headold_head

B4

C

5

A

Функция try_reclaim освобождения удалённых узлов

56threads_in_pop 2

43

delete_list

threads_in_pop 2

2

A возобновляет выполнение и захватывает список на удаление. После этого он должен 2-й раз проверить,

один ли он в pop()

headold_head

B2

5

A

43

headold_head

B2

delete_list A2 5

delete_list

B возобновляет выполнение, выполняет CAS и переходит

к 3 узлу

Реализация стека, свободного от блокировок, на основе указателей опасности

57

Указатели опасности (hazard pointers)

58

4

old_head

321

head->next

43

Поток А выполняет удаление узла из вершины стека и помечает узел 1

как узел, который он использует.

old_head old_head->next

Поток А был вытеснен и другие потоки удалили два узла из стека, но не

освобождают память из-под первого узла.

2

head

head

1

A “понимает”, что головной узел head изменился и нужно

выполнить compare_exchange

43

old_head old_head->next

21

Функция pop на основе указателей опасности

59

std::shared_ptr<T> pop() { std::atomic<void*>& hp = get_hazard_pointer_for_current_thread();

// установить указатель опасности перед чтением указателя, // который мы собираемся разыменовывать node* old_head = head.load(); node* temp; do { temp = old_head; hp.store(old_head); old_head = head.load(); } while (old_head != temp); // ...}

Функция pop на основе указателей опасности

60

std::shared_ptr<T> pop() { std::atomic<void*>& hp = get_hazard_pointer_for_current_thread();

node* old_head = head.load(); do { node* temp; do { temp = old_head; hp.store(old_head); // устанавливаем УО old_head = head.load(); } while (old_head != temp); } while (old_head && // получаем узел !head.compare_exchange_strong(old_head, old_head->next)); hp.store(nullptr);

Указатели опасности (hazard pointers)

61

4

old_head

321head

hp

Указатели опасности (hazard pointers)

62

4

old_head

321head

temp = old_head

temp

hp

Указатели опасности (hazard pointers)

63

4

old_head

321head

hp

temp = old_head

temp

hp.store(old_head)

Указатели опасности (hazard pointers)

64

4

old_head

321

temp = old_head

head

temp

hp.store(old_head)

old_head = head.load()“old old_head”

“new old_head”

hp

Указатели опасности (hazard pointers)

65

4

old_head

321

temp = old_head

head

temp

hp.store(old_head)

old_head = head.load()“old old_head”

“new old_head”

hp

== ?

Таким образом, внутренний цикл гарарантирует то, что указатель опасности будет указывать на тот головной элемент head, с котором

мы будем работать (сдвигать указатель на следующий элемент)

Проверка позволяет определить, не изменился ли головной элемент с тех пор, когда мы запомнили его в указателе опасности.

Указатели опасности (hazard pointers)

66

4

old_head

321

temp = old_head

head

temp

hp.store(old_head)

old_head = head.load()“old old_head”

“new old_head”

hp

== ?

Во внешнем цикле сдвигаем указатель с head на следующий элемент с уверенностью, что никто не подменит элемент head.

Указатели опасности (hazard pointers)

67

После того, как поток А успешно выполнил compare_exchange,

указатель опасности можно обнулять hp.store(nullptr), т.к. никто пока

не сможет удалить old_head, кроме А, поскольку head изменён потоком А

43

old_head old_head->next

21

Вариант 1

43

old_head->next

1

Вариант 2

old_head

2

Функция pop на основе указателей опасности

68

std::shared_ptr<T> res; if (old_head) { res.swap(old_head->data); // извлекаем данные

if (outstanding_hazard_pointers_for(old_head)) // если опасно, удаляем потом reclaim_later(old_head); else // если не опасно, удаляем сейчас delete old_head;

// пробуем удалить узлы, какие можно удалить delete_nodes_with_no_hazards(); } return res;}

Реализация указателей опасности

69

4321head

1 5 6 7 m432

max_hazard_pointers

Реализация указателей опасности

70

4321head

1 5 6 7 m432

Указатели опасности, m = max_hazard_pointers

пустой?нет

if (hazard_pointers[i].id. compare_exchange_strong( old_id, std::this_thread::get_id()))

thread_local hp

Реализация указателей опасности

71

4321head

1 5 6 7 m432

Указатели опасности, m = max_hazard_pointers

пустой?да

if (hazard_pointers[i].id. compare_exchange_strong( old_id, std::this_thread::get_id()))

thread_local hp

Реализация указателей опасности

72

4321head

1 5 6 7 m432

Указатели опасности, m = max_hazard_pointers

пустой?да

if (hazard_pointers[i].id. compare_exchange_strong( old_id, std::this_thread::get_id()))

thread_local hp

Реализация указателей опасности

73

const auto max_hazard_pointers = 100;

struct hazard_pointer { std::atomic<std::thread::id> id; std::atomic<void*> pointer;};

hazard_pointer hazard_pointers[max_hazard_pointers];

class hp_owner { hazard_pointer* hp;

public: hp_owner(hp_owner const&) = delete; hp_owner operator=(hp_owner const&) = delete;

Реализация указателей опасности

74

hp_owner(): hp{nullptr} { for (auto i = 0; i < max_hazard_pointers; i++) { std::thread::id old_id; // пустой незанятый УО

// если i-й УО не занят, завладеть им, записав в него // свой идентификатор потока if (hazard_pointers[i].id.compare_exchange_strong( old_id, std::this_thread::get_id())) { hp = &hazard_pointers[i]; // я владею i-м УО break; } }

// таблица УО закончилась, указателей нам не досталось if (!hp) throw std::runtime_error("No hazard ptrs available");}

Реализация указателей опасности

75

hp_owner(): hp{nullptr} { for (auto i = 0; i < max_hazard_pointers; i++) { std::thread::id old_id; if (hazard_pointers[i].id.compare_exchange_strong( old_id, std::this_thread::get_id())) { hp = &hazard_pointers[i]; break; } } if (!hp) throw std::runtime_error("No hazard ptrs available");}std::atomic<void*>& get_pointer() { return hp->pointer;}~hp_owner() { hp->pointer.store(nullptr); hp->id.store(std::thread::id());}

Реализация указателей опасности

76

// вернуть указатель опасности для текущего потокаstd::atomic<void*>& get_hazard_pointer_for_current_thread() { thread_local static hp_owner hazard; return hazard.get_pointer();}

Реализация указателей опасности

77

// вернуть указатель опасности для текущего потокаstd::atomic<void*>& get_hazard_pointer_for_current_thread() { thread_local static hp_owner hazard; return hazard.get_pointer();}

// проверить, не ссылается ли на указатель какой-то из УОbool outstanding_hazard_pointers_for(void* p) { for (auto i = 0; i < max_hazard_pointers; i++) { if (hazard_pointers[i].pointer.load() == p) { return true; } } return false;}

Реализация функции освобождения памяти

78

template <typename T>void do_delete(void* p) { delete static_cast<T*>(p);}

struct data_to_reclaim { // обёртка над данными для

void* data; // помещения в список удаления

std::function<void(void*)> deleter;

data_to_reclaim* next;

template<typename T> data_to_reclaim(T* p): data{p}, deleter{&do_delete<T>}, next{0} { }

~data_to_reclaim() { deleter(data); }};

std::atomic<data_to_reclaim*> nodes_to_reclaim;

Реализация функции освобождения памяти

79

// добавить элемент в список на удалениеvoid add_to_reclaim_list(data_to_reclaim* node) { node->next = nodes_to_reclaim.load();

while (!nodes_to_reclaim.compare_exchange_weak( node->next, node));}

// удалить элемент позжеtemplate<typename T>void reclaim_later(T* data) { add_to_recalim_list(new data_to_reclaim(data));}

Реализация функции освобождения памяти

80

void delete_nodes_with_no_hazards() { // захватить текущий список data_to_reclaim* current = nodes_to_reclaim.exchange(nullptr);

while (current) { data_to_reclaim* const next = current->next;

if (!outstanding_hazard_pointers_for(current->data)) // если не опасно, удалить сейчас delete current; else // если опасно удалить потом add_to_reclaim_list(current);

current = next; }}

Реализация функции освобождения памяти

81

4321

nodes_to_reclaim

Реализация функции освобождения памяти

82

nodes_to_reclaim

4321current

current = nodes_to_reclaim. exchange(nullptr);

Реализация функции освобождения памяти

83

nodes_to_reclaim

432current

1

1

add_to_reclaim_list(current);

Реализация функции освобождения памяти

84

nodes_to_reclaim

432current

1

delete current;

Реализация функции освобождения памяти

85

nodes_to_reclaim

43current

1 5

add_to_reclaim_list() при выполнении pop()

Реализация функции освобождения памяти

86

nodes_to_reclaim

4current

1 5 3

add_to_reclaim_list(current);

1

Реализация функции освобождения памяти

87

nodes_to_reclaim

4current

1 5 3

Реализация функции освобождения памяти

88

nodes_to_reclaim

current

1 5 3

Недостатки указетелей опасности

89

1. Просмотр массива указателей опаности требует в худшем случае max_hazard_pointers атомарных переменных.

2. Атомарные операции могут работать медленнее эквивалентных обычных операций

3. При освобождении узла также требуется просмотреть список указателей опаности, т.е. max_hazard_pointers в худшем случае.

Функция pop дорогостоящая. Решения?

Недостатки указетелей опасности

90

1. Просмотр массива указателей опаности требует в худшем случае max_hazard_pointers атомарных переменных.

2. Атомарные операции могут работать медленнее эквивалентных обычных операций

3. При освобождении узла также требуется просмотреть список указателей опаности, т.е. max_hazard_pointers в худшем случае.

Функция pop дорогостоящая. Решения?

1. Вместо просмотра max_hazard_pointers в каждом pop(), проверяем 2 * max_hazard_pointers через каждые max_hazard_pointers вызовов pop() и освобождаем не менее max_hazard_pointers. В среднем проверяем два узла при каждом вызове pop() и один освобождаем.

2. Каждый поток хранит собственный список освобождения в локальных данных потока. Это потребует выделения памяти под max_hazard_pointers2 узлов.

Реализация стека, свободного от блокировок, с помощью умного указателя

91

Реализация на основе атомарного умного указателя

92

▪ Удалять узлы можно только при отсутствии обращения к ним из других потоков

▪ Если на узел нет ссылки, то его можно удаляь

Реализация на основе атомарного умного указателя

93

▪ Удалять узлы можно только при отсутствии обращения к ним из других потоков

▪ Если на узел нет ссылки, то его можно удаляь

▪ Умный указатель shared_ptr как раз решает эту задачу!

Реализация на основе атомарного умного указателя

94

▪ Удалять узлы можно только при отсутствии обращения к ним из других потоков

▪ Если на узел нет ссылки, то его можно удаляь

▪ Умный указатель shared_ptr как раз решает эту задачу!

...

▪ Но, к сожалению, атомарные операции shared_ptr в большинстве реализаций не свободны от блокировок.

Реализация на основе атомарного умного указателя

95

template <typename T>class lfstack {private:

struct node { std::shared_ptr<T> data; std::shared_ptr<node> next; node(T const& _data): data(std::make_shared<T>(_data)) { } };

std::shared_ptr<node> head;

Реализация на основе атомарного умного указателя

96

...

void push(T const& data) { std::shared_ptr<node> const new_node = std::make_shared<node>(data); new_node->next = head.load();

while (!std::atomic_compare_exchange_weak(&head, &nead_node->next, new_node)); }

std::shared_ptr<T> pop() { std::shared_ptr<node> old_head = std::atomic_load(&head);

while (old_head && !std::atomic_compare_exchange_weak(&head, &old_head, old_head->next));

return old_head ? old_head->data : std::shared_ptr<T>(); }

Реализация стека, свободного от блокировок, с помощью подсчёта ссылок

97

Двойной счётчик ссылок

98

counted_node_ptr

node

internal_count

data

next

external_count

▪ При начале каждого чтения внешний счётчик увеличивается.

▪ При завершении чтения внутренний счётчик уменьшается.

▪ При удалении узла внутренний счетчик увеличивается на величину внешнего минус 1, а внешний отбрасывается.

▪ Если внутренний счётчик равен 0, узел можно удалять.

Двойной счётчик ссылок

99

head

1 2 3

Двойной счётчик ссылок

100

template<typename T>class lfstack {private:

struct counted_node_ptr { int external_count; node* ptr; };

struct node { std::shared_ptr<T> data; std::atomic<int> internal_count; counted_node_ptr next; node(T const& _data): data(std::make_shared<T>(_data)), internal_count(0) {} };

std::atomic<counted_node_ptr> head;

Двойной счётчик ссылок

101

~lfstack() { while (pop()); }

void push(T const& data) { counted_node_ptr new_node; new_node.ptr = new node(data); new_node.external_count = 1; new_node.ptr->next = head.load(); while (!head.compare_exchange_weak(new_node.ptr->next, new_node)); }};

Двойной счётчик ссылок

102

template <typename T>class lfstack {private:

// Увеличение внешнего счётчика void increase_head_count(counted_node_ptr& old_counter) { counted_node_ptr new_counter;

do { new_counter = old_counter; ++new_counter.external_count; } while (!head.compare_exchange_strong(old_counter, new_counter));

old_counter.external_count = new_counter.external_count; }

public: std::shared_ptr<T> pop() { counted_node_ptr old_head = head.load();

for (;;) { increase_head_count(old_head); node* const ptr = old_head.ptr; if (!ptr) return std::shared_ptr<T>();

if (head.compare_exchange_strong(old_head, ptr->next)) { std::shared_ptr<T> res; res.swap(ptr->data); int const count_increase = old_head.external_count - 2;

if (ptr->internal_count.fetch_add(count_increase) == -count_increase) delete ptr; return res;

} else if (ptr->internal_count.fetch_sub(1) == 1) delete ptr; } }

Двойной счётчик ссылок

103

public: std::shared_ptr<T> pop() { counted_node_ptr old_head = head.load();

for (;;) { increase_head_count(old_head); node* const ptr = old_head.ptr; if (!ptr) return std::shared_ptr<T>();

if (head.compare_exchange_strong(old_head, ptr->next)) { std::shared_ptr<T> res; res.swap(ptr->data); int const count_increase = old_head.external_count - 2;

if (ptr->internal_count.fetch_add(count_increase) == -count_increase) delete ptr; return res;

} else if (ptr->internal_count.fetch_sub(1) == 1) delete ptr; } }

Двойной счётчик ссылок

104

1. Увеличить внешний счётчик2. Разыменовать указатель3. Проверить указатель на пустоту

public: std::shared_ptr<T> pop() { counted_node_ptr old_head = head.load();

for (;;) { increase_head_count(old_head); node* const ptr = old_head.ptr; if (!ptr) return std::shared_ptr<T>();

if (head.compare_exchange_strong(old_head, ptr->next)) { std::shared_ptr<T> res; res.swap(ptr->data); int const count_increase = old_head.external_count - 2;

if (ptr->internal_count.fetch_add(count_increase) == -count_increase) delete ptr; return res;

} else if (ptr->internal_count.fetch_sub(1) == 1) delete ptr; } }

Двойной счётчик ссылок

105

Попытаться выполнить удаление узла

1. Если получилось, забрать данные2. Прибавить внутренний счётчик к внешнему3. Если счётчик стал равным 0, удалить узел4. Вернуть результат (даже если счётчик не стал равным 0)

public: std::shared_ptr<T> pop() { counted_node_ptr old_head = head.load();

for (;;) { increase_head_count(old_head); node* const ptr = old_head.ptr; if (!ptr) return std::shared_ptr<T>();

if (head.compare_exchange_strong(old_head, ptr->next)) { std::shared_ptr<T> res; res.swap(ptr->data); int const count_increase = old_head.external_count - 2;

if (ptr->internal_count.fetch_add(count_increase) == -count_increase) delete ptr; return res;

} else if (ptr->internal_count.fetch_sub(1) == 1) delete ptr; } }

Двойной счётчик ссылок

106

Если не получилось выполнить удаление узла (какой-то поток удалил узел раньше нас)1. Уменьшить счётчик ссылок на 1.2. Если другие потоки на узел не ссылаются, освободить память

(убрать за тем потоком, который выполнил удаление)

0 0

Двойной счётчик ссылок

107

1

0

1 1

head

1 2 3

Сценарий 1:

Поток А эксклюзивно удаляет узел.

Другие потоки ему не мешают.

0 0

Двойной счётчик ссылок

108

2 1 1

head

1 2 3

Поток A:increase_head_count(old_head)node* const = old_head.ptr

0

0 0 0

Двойной счётчик ссылок

109

2 1 1

head

1 2 3

Поток A:head.compare_exchange(old_head, ptr->next)

0 0 0

Двойной счётчик ссылок

110

2 0 0

head

1 2 3

Поток A:count_increase = 2 - 2 = 0internal_count = 0 + 0 = 0

0 0 0

Двойной счётчик ссылок

111

2 0 0

head

1 2 3

Поток A:delete ptr

0 0 0

Двойной счётчик ссылок

112

1 1 1

head

1 2 3

Сценарий 2:

Потоки А и В одновременно удаляют узел.

Потоку А удаётся выполнить удаление узла вперёд B.

Поток В успевает выйти из pop до того,

как А попробует освободить узел.

0 0 0

Двойной счётчик ссылок

113

2 1 1

head

1 2 3

Поток B: increase_head_count(old_head)

0 0 0

Двойной счётчик ссылок

114

3 1 1

head

1 2 3

Поток B: increase_head_count(old_head)

Поток A: increase_head_count(old_head) head.compare_exchange(...)

-1 0 0

Двойной счётчик ссылок

115

3 0 0

head

1 2 3

Поток B: increase_head_count(old_head)

Поток B: internal_count.fetch_sub(1)

Поток A: increase_head_count(old_head) head.compare_exchange(...)

0 0 0

Двойной счётчик ссылок

116

x 0 0

head

1 2 3

Поток A: count_increase = 3 - 2 = 1 internal_count = -1 + 1 = 0

Поток B: increase_head_count(old_head)

Поток B: internal_count = 0 - 1 = -1

Поток A: increase_head_count(old_head) head.compare_exchange(...)

0 0 0

Двойной счётчик ссылок

117

x 0 0

head

1 2 3

Поток A: count_increase = 3 - 2 = 1 internal_count = -1 + 1 = 0

Поток B: increase_head_count(old_head)

Поток B: internal_count = 0 - 1 = -1

Поток A: increase_head_count(old_head) head.compare_exchange(...)

Поток A: delete ptr

0 0 0

Двойной счётчик ссылок

118

1 1 1

head

1 2 3

Сценарий 3:

Потоки А и В одновременно удаляют узел.

Потоку А удаётся выполнить удаление узла вперёд B.

Поток В не успевает выйти из pop, когда А пытается

освободить узел, и поэтому А узел не освобождает.

Зато поток В, последним покидая узел,

освобождает память из-под узла, удалённого А.

0 0 0

Двойной счётчик ссылок

119

3 1 1

head

1 2 3

Поток B: increase_head_count(old_head)

Поток A: increase_head_count(old_head) head.compare_exchange(...)

1 0 0

Двойной счётчик ссылок

120

x 1 1

head

1 2 3

Поток B: increase_head_count(old_head)

Поток A: increase_head_count(old_head) head.compare_exchange(...)

Поток A: count_increase = 3 - 2 = 1 internal_count = 0 + 1 = 1

Поток A узел не освобождает

0 0 0

Двойной счётчик ссылок

121

x 1 1

head

1 2 3

Поток B: increase_head_count(old_head)

Поток A: increase_head_count(old_head) head.compare_exchange(...)

Поток A: count_increase = 3 - 2 = 1 internal_count = 0 + 1 = 1

Поток A узел не освобождает

Поток B: internal_count = 1 - 1 = 0 delete ptr

Узел освобождает поток B

Двойной счётчик ссылок - проблема

122

template<typename T>class lfstack {private:

struct counted_node_ptr { int external_count; node* ptr; };

struct node { std::shared_ptr<T> data; std::atomic<int> internal_count; counted_node_ptr next; node(T const& _data): data(std::make_shared<T>(_data)), internal_count(0) {} };

std::atomic<counted_node_ptr> head;

Структура может не поддерживать выполнение атомарных операций без блокировок!

Применение модели памяти С++ для стека, свободного от блокировок

123

Применение модели памяти С++

124

void push(T const& data) { counted_node_ptr new_node; new_node.ptr = new node(data); new_node.external_count = 1; new_node.ptr->next = head.load(std::memory_order_relaxed);

while (!head.compare_exchange_weak(new_node.ptr->next, new_node));

void increase_head_count(counted_node_ptr& old_counter) { counted_node_ptr new_counter;

do { new_counter = old_counter; ++new_counter.external_count; } while (!head.compare_exchange_strong(old_counter, new_counter));

old_counter.external_count = new_counter.external_count;}

Применение модели памяти С++

125

void push(T const& data) { counted_node_ptr new_node; new_node.ptr = new node(data); new_node.external_count = 1; new_node.ptr->next = head.load(std::memory_order_relaxed);

while (!head.compare_exchange_weak(new_node.ptr->next, new_node));

void increase_head_count(counted_node_ptr& old_counter) { counted_node_ptr new_counter;

do { new_counter = old_counter; ++new_counter.external_count; } while (!head.compare_exchange_strong(old_counter, new_counter));

old_counter.external_count = new_counter.external_count;}

Подготовка данных

Установка head (“флага”)

Проверка head (“флага”)

Работа с добавленным элементом

Применение модели памяти С++

126

void push(T const& data) { counted_node_ptr new_node; new_node.ptr = new node(data); new_node.external_count = 1; new_node.ptr->next = head.load(std::memory_order_relaxed);

while (!head.compare_exchange_weak(new_node.ptr->next, new_node, std::memory_order_release, std::memory_order_relaxed));

void increase_head_count(counted_node_ptr& old_counter) { counted_node_ptr new_counter;

do { new_counter = old_counter; ++new_counter.external_count; } while (!head.compare_exchange_strong(old_counter, std::memory_order_acquire, std::memory_order_relaxed, new_counter));

old_counter.external_count = new_counter.external_count;}

Подготовка данных

Установка head (“флага”)

Работа с добавленным элементом

Проверка head (“флага”)

Применение модели памяти С++

127

std::shared_ptr<T> pop() { counted_node_ptr old_head = head.load(); for (;;) { increase_head_count(old_head); node* const ptr = old_head.ptr; if (!ptr) return std::shared_ptr<T>();

if (head.compare_exchange_strong(old_head, ptr->next)) { std::shared_ptr<T> res; res.swap(ptr->data); int const count_increase = old_head.external_count - 2;

if (ptr->internal_count.fetch_add(count_increase) == -count_increase) delete ptr; return res;

} else if (ptr->internal_count.fetch_sub(1) == 1) delete ptr; }}

Чтение указателя

Применение модели памяти С++

128

std::shared_ptr<T> pop() { counted_node_ptr old_head = head.load(); for (;;) { increase_head_count(old_head);

node* const ptr = old_head.ptr; if (!ptr) return std::shared_ptr<T>();

if (head.compare_exchange_strong(old_head, ptr->next, std::memory_order_relaxed)) { std::shared_ptr<T> res; res.swap(ptr->data); int const count_increase = old_head.external_count - 2;

if (ptr->internal_count.fetch_add(count_increase) == -count_increase) delete ptr; return res;

} else if (ptr->internal_count.fetch_sub(1) == 1) delete ptr; }}

Чтение указателя

acquire не нужен, т.к. захват выполнен в

increase_head_count

Применение модели памяти С++

129

std::shared_ptr<T> pop() { counted_node_ptr old_head = head.load(); for (;;) { increase_head_count(old_head); node* const ptr = old_head.ptr; if (!ptr) return std::shared_ptr<T>();

if (head.compare_exchange_strong(old_head, ptr->next, std::memory_order_relaxed)) { std::shared_ptr<T> res; res.swap(ptr->data); int const count_increase = old_head.external_count - 2;

if (ptr->internal_count.fetch_add(count_increase) == -count_increase) delete ptr; return res;

} else if (ptr->internal_count.fetch_sub(1) == 1) delete ptr; }}

Извлечение данных

Удаление должно выполняться после извлечения данных

Применение модели памяти С++

130

std::shared_ptr<T> pop() { counted_node_ptr old_head = head.load(); for (;;) { increase_head_count(old_head); node* const ptr = old_head.ptr; if (!ptr) return std::shared_ptr<T>();

if (head.compare_exchange_strong(old_head, ptr->next, std::memory_order_relaxed)) { std::shared_ptr<T> res; res.swap(ptr->data); int const count_increase = old_head.external_count - 2;

if (ptr->internal_count.fetch_add(count_increase, std::memory_order_release) == -count_increase) delete ptr; return res;

} else if (ptr->internal_count.fetch_sub(1) == 1) delete ptr; }}

Удаление должно выполняться после извлечения данных

Извлечение данных

Применение модели памяти С++

131

std::shared_ptr<T> pop() { counted_node_ptr old_head = head.load(); for (;;) { increase_head_count(old_head); node* const ptr = old_head.ptr; if (!ptr) return std::shared_ptr<T>();

if (head.compare_exchange_strong(old_head, ptr->next, std::memory_order_relaxed)) { std::shared_ptr<T> res; res.swap(ptr->data); int const count_increase = old_head.external_count - 2;

if (ptr->internal_count.fetch_add(count_increase, std::memory_order_release) == -count_increase) delete ptr; return res; } else if (ptr->internal_count.fetch_sub(1, std::memory_order_acquire) == 1)

delete ptr; }

Удаление должно выполняться после извлечения данных

Извлечение данных

Применение модели памяти С++

132

std::shared_ptr<T> pop() { counted_node_ptr old_head = head.load(); for (;;) { increase_head_count(old_head); node* const ptr = old_head.ptr; if (!ptr) return std::shared_ptr<T>();

if (head.compare_exchange_strong(old_head, ptr->next, std::memory_order_relaxed)) { std::shared_ptr<T> res; res.swap(ptr->data); int const count_increase = old_head.external_count - 2;

if (ptr->internal_count.fetch_add(count_increase, std::memory_order_release) == -count_increase) delete ptr; return res; } else if (ptr->internal_count.fetch_sub(1, std::memory_order_relaxed) == 1) ptr->internal_count.load(std::memory_order_acquire);

delete ptr; }

Достаточно вставить операцию захвата-загрузки, чтобы удалить ptr после извлечения

данных

Извлечение данных

fetch_sub входит в последовательность

освобождений, поэтому “не мешает” acquire

top related