ПВТ - осень 2014 - Лекция 7. Многопоточное программирование без блокировок. Модель потребитель-производитель. Потокобезопасный стек
- 1. Лекция 7. Многопоточное
программирование без блокировок.
Модель потребитель-производитель.
Потокобезопасный стек
Пазников Алексей Александрович
Кафедра вычислительных систем СибГУТИ
Сайт курса: http://cpct.sibsutis.ru/~apaznikov/teaching/
Вопросы: https://piazza.com/sibsutis.ru/fall2014/pct14/home
Параллельные вычислительные технологии
Осень 2014 (Parallel Computing Technologies, PCT 14)
- 2. Программирование без
блокировок
2
Если вы думаете, что программирование без блокировок
это просто, значит или вы - один из тех 50, которые умеют
это делать, или же используете атомарные инструкции
недостаточно аккуратно.
Герб Саттер
- 4. Цели многопоточного программирования без блокировок
▪ Повышение масштабируемости путём
сокращения блокировок и ожиданий в алгоритмах
и структурах данных.
▪ Решение проблем, связанных с блокировками:
▫ Гонки: нужно не забыть заблокировать,
причём именно нужный участок кода
▫ Дедлоки: необходимо запирать в нужном
порядке различные потоки
▫ Сложность выбора критической секции
(простота или масштабируемость)
▫ Голоданеие, инверсия приоритетов и др.
4
- 5. Виды алгоритмов, свободных от блокировок
▪ Свободные от ожиданий (wait-free). “Никто никогда не ждёт”.
Каждая операция завершается за N шагов без каких-либо условий.
Гарантии:
▫ максимум пропускной способности системы
▫ отсутствие голодания
▪ Свободные от блокировок (lock-free). “Всегда какой-то из потоков
работает”. Каждый шаг приближает итоговое решение. Гарантии:
▫ максимум пропускной способности системы
▫ отсутствие голодания (один поток может постоянно ожидать)
▪ Свободные от остановок (obstruction-free). “Поток работает, если
нет конфликтов”. За ограниченное число шагов один поток, при
условии, что другие потоки остановлены, достигает результата.
▫ Все потоки не блокируются из-за проблем (задержек, ошибок) с
другими потоками.
▫ Не гарантируется прогресс, если одновременно работают два
или больше потоков. 5
- 6. Реализация спинлока на основе атомарного флага
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
- 9. Производитель-потребитель на основе блокировок
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
- 10. Производитель-потребитель на основе блокировок
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
- 20. Производитель
20
curr = 0; // указатель на текущий слот
while (ThereAreMoreTasks()) {
task = AllocateAndBuildNewTask();
while (slot[curr] != null) // если null, то проверить
curr = (curr + 1) % numOfConsumers; // следующий слот
slot[curr] = task;
sem[curr].signal();
}
- 21. Производитель
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. Потребитель
22
task = null;
while (task != done)
// Дождаться, когда слот будет полным
while ((task = slot[mySlot]) == null)
sem[mySlot].wait();
if (task != done) {
slot[mySlot] = null; // помечаем слот пустым
DoWork(task); // выполняем задачу
} // вне критической секции
}
- 23. Потребитель
23
task = null;
while (task != done)
// Дождаться, когда слот будет полным
while ((task = slot[mySlot]) == null)
sem[mySlot].wait();
if (task != done) {
slot[mySlot] = null; // помечаем слот пустым
DoWork(task); // выполняем задачу
} // вне критической секции
}
Как применить модель памяти С++?
- 24. Производитель, модель памяти С++
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. Птребитель, модель памяти С++
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. Производитель-потребитель, класс алгоритма
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. Производитель-потребитель, класс алгоритма
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. Производитель-потребитель без блокировок
28
task = null;
while (task != done)
// Дождаться, когда слот будет полным
while ((task = slot[mySlot]) == null)
sem[mySlot].wait();
if (task != done) {
slot[mySlot] = null;
DoWork(task);
}
}
можно ли поменять две строки?
нужно ли это сделать?
- 30. Стек, свободный от блокировок
30
T T T T
head
1. Конструктор
2. Деструктор
3. Поиск узла (find)
4. Добавление узла (push)
5. Удаление узла (pop)
- 31. Стек, свободный от блокировок
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. Конструктор и деструктор
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;
}
}
- 33. Функция 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} {}
};
- 36. Функция 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.
- 40. Функция 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;
}
- 41. Функция 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;
}
- 42. Функция 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>();
}
- 43. Функция 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>();
}
- 46. Проблема АВА
46
4
old_head
321
head->next
43
Поток А был вытеснен и другие
потоки удалили два узла из стека
Поток А выполняет удаление узла
из вершины стека
435
old_head old_head->next
old_head old_head->next
Некий поток добавил новый узел и
аллокатор выделил под него ту же память
- 47. Проблема АВА
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. Проблема АВА
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. Решения проблемы АВА
49
1. “Ленивый” сборщик мусора
2. Указатели опасности
3. Счётчик ссылок на элемент
4. Сделать узлы уникальными
5. Вообще не удалять узлы
6. Добавление дополнительных узлов
7. и т.д.
- 50. Функция pop (наивная)
50
// количество потоков, выполняющих pop
std::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;
}
- 51. Функция 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;
}
}
- 52. Функция 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;
}
}
- 53. Функция 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);
}
- 54. Функция try_reclaim освобождения удалённых узлов
54
4
head
321
delete_list 0threads_in_pop
4
head
321
delete_list
threads_in_pop 1
5
5
A A удаляет узел 1 и
вытесняется в pop() после
1-го чтения threads_in_pop
- 55. Функция try_reclaim освобождения удалённых узлов
55
4
head
32
delete_list 2threads_in_pop
43
delete_list
threads_in_pop 2
5
2
С удаляет узел и
продолжает работать до
момента выхода из pop()
old_head
B
B вызывает pop() и
вытесняется после 1-го
чтения head
A
head
old_head
B
4
C
5
A
- 56. Функция try_reclaim освобождения удалённых узлов
56threads_in_pop 2
43
delete_list
threads_in_pop 2
2
A возобновляет выполнение
и захватывает список на
удаление. После этого он
должен 2-й раз проверить,
один ли он в pop()
head
old_head
B
2
5
A
43
head
old_head
B
2
delete_list A
2 5
delete_list
B возобновляет выполнение,
выполняет CAS и переходит
к 3 узлу
- 58. Указатели опасности (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
- 59. Функция 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);
// ...
}
- 60. Функция 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);
- 64. Указатели опасности (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
- 65. Указатели опасности (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, с котором
мы будем работать (сдвигать указатель на следующий элемент)
Проверка позволяет определить, не изменился ли головной элемент с
тех пор, когда мы запомнили его в указателе опасности.
- 66. Указатели опасности (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.
- 67. Указатели опасности (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
- 68. Функция 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;
}
- 73. Реализация указателей опасности
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. Реализация указателей опасности
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. Реализация указателей опасности
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. Реализация указателей опасности
76
// вернуть указатель опасности для текущего потока
std::atomic<void*>&
get_hazard_pointer_for_current_thread() {
thread_local static hp_owner hazard;
return hazard.get_pointer();
}
- 77. Реализация указателей опасности
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. Реализация функции освобождения памяти
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. Реализация функции освобождения памяти
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. Реализация функции освобождения памяти
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;
}
}
- 89. Недостатки указетелей опасности
89
1. Просмотр массива указателей опаности требует в худшем случае
max_hazard_pointers атомарных переменных.
2. Атомарные операции могут работать медленнее эквивалентных
обычных операций
3. При освобождении узла также требуется просмотреть список
указателей опаности, т.е. max_hazard_pointers в худшем случае.
Функция pop дорогостоящая. Решения?
- 90. Недостатки указетелей опасности
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
узлов.
- 92. Реализация на основе атомарного умного указателя
92
▪ Удалять узлы можно только при отсутствии
обращения к ним из других потоков
▪ Если на узел нет ссылки, то его можно
удаляь
- 93. Реализация на основе атомарного умного указателя
93
▪ Удалять узлы можно только при отсутствии
обращения к ним из других потоков
▪ Если на узел нет ссылки, то его можно
удаляь
▪ Умный указатель shared_ptr как раз
решает эту задачу!
- 94. Реализация на основе атомарного умного указателя
94
▪ Удалять узлы можно только при отсутствии
обращения к ним из других потоков
▪ Если на узел нет ссылки, то его можно
удаляь
▪ Умный указатель shared_ptr как раз
решает эту задачу!
...
▪ Но, к сожалению, атомарные операции
shared_ptr в большинстве реализаций не
свободны от блокировок.
- 95. Реализация на основе атомарного умного указателя
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. Реализация на основе атомарного умного указателя
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>();
}
- 100. Двойной счётчик ссылок
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. Двойной счётчик ссылок
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. Двойной счётчик ссылок
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;
}
- 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;
}
}
Двойной счётчик ссылок
103
- 104. 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. Проверить указатель на пустоту
- 105. 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)
- 106. 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. Если другие потоки на узел не ссылаются, освободить память
(убрать за тем потоком, который выполнил удаление)
- 107. 0 0
Двойной счётчик ссылок
107
1
0
1 1
head
1 2 3
Сценарий 1:
Поток А эксклюзивно удаляет узел.
Другие потоки ему не мешают.
- 108. 0 0
Двойной счётчик ссылок
108
2 1 1
head
1 2 3
Поток A:
increase_head_count(old_head)
node* const = old_head.ptr
0
- 109. 0 0 0
Двойной счётчик ссылок
109
2 1 1
head
1 2 3
Поток A:
head.compare_exchange(old_head,
ptr->next)
- 110. 0 0 0
Двойной счётчик ссылок
110
2 0 0
head
1 2 3
Поток A:
count_increase = 2 - 2 = 0
internal_count = 0 + 0 = 0
- 111. 0 0 0
Двойной счётчик ссылок
111
2 0 0
head
1 2 3
Поток A:
delete ptr
- 112. 0 0 0
Двойной счётчик ссылок
112
1 1 1
head
1 2 3
Сценарий 2:
Потоки А и В одновременно удаляют узел.
Потоку А удаётся выполнить удаление узла вперёд B.
Поток В успевает выйти из pop до того,
как А попробует освободить узел.
- 113. 0 0 0
Двойной счётчик ссылок
113
2 1 1
head
1 2 3
Поток B: increase_head_count(old_head)
- 114. 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(...)
- 115. -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(...)
- 116. 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(...)
- 117. 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
- 118. 0 0 0
Двойной счётчик ссылок
118
1 1 1
head
1 2 3
Сценарий 3:
Потоки А и В одновременно удаляют узел.
Потоку А удаётся выполнить удаление узла вперёд B.
Поток В не успевает выйти из pop, когда А пытается
освободить узел, и поэтому А узел не освобождает.
Зато поток В, последним покидая узел,
освобождает память из-под узла, удалённого А.
- 119. 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(...)
- 120. 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 узел
не освобождает
- 121. 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. Двойной счётчик ссылок - проблема
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;
Структура может не поддерживать выполнение
атомарных операций без блокировок!
- 124. Применение модели памяти С++
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. Применение модели памяти С++
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. Применение модели памяти С++
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. Применение модели памяти С++
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. Применение модели памяти С++
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. Применение модели памяти С++
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. Применение модели памяти С++
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. Применение модели памяти С++
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. Применение модели памяти С++
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