1 / 1 / 0
Регистрация: 08.08.2021
Сообщений: 18
1

Энтони Уильямс. - 7.2.5. Применение модели памяти к свободному от блокировок стеку

04.07.2022, 15:12. Показов 2754. Ответов 4

Author24 — интернет-сервис помощи студентам
Доброго времени суток.
Начал читать книгу - Параллельное программирование на С++ в действии (перевод на русский язык ДМК Пресс).
Столкнулся с проблемой в понимании раздела 7.2.5, где приводится
реализация стека с применением модели памяти к свободному от блокировок.

Приведу листинг из книги:
Кликните здесь для просмотра всего текста
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include <atomic>
#include <memory>
 
template<typename T>
class lock_free_stack
{
private:
    struct node;
    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;
    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,
                  std::memory_order_acquire,
                  std::memory_order_relaxed));
        old_counter.external_count=new_counter.external_count;
    }
public:
    ~lock_free_stack()
    {
        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(std::memory_order_relaxed);
            while(!head.compare_exchange_weak(
                      new_node.ptr->next,new_node,
                      std::memory_order_release,
                      std::memory_order_relaxed));
    }
    std::shared_ptr<T> pop()
    {
        counted_node_ptr old_head=
            head.load(std::memory_order_relaxed);
        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_add(
                        -1,std::memory_order_relaxed)==1)
            {
                ptr->internal_count.load(std::memory_order_acquire);
                delete ptr;
            }
        }
    }
};


И текст из книги, описывающий последнее ветвление кода:
Кликните здесь для просмотра всего текста

Мы почти закончили, осталось только рассмотреть функции, в
которых используются операции fetch_add(), изменяющие счетчик
ссылок. Поток, который добрался до возврата данных из узла, может
продолжать в твердой уверенности, что никакой другой поток не смо-
жет модифицировать хранящиеся в узле данные. Однако любой поток,
который потерпел неудачу при извлечении данных, знает, что какой-то
другой поток данные в узле модифицировал; он использовал функцию
swap() для извлечения данных. Следовательно, чтобы предотвратить
гонку за данными мы должны гарантировать, что swap() происходит-
раньше delete. Чтобы добиться этого, проще всего задать семантику
std::memory_order_release при вызове fetch_add() в ветви, где
мы возвращаем данные, и семантику std::memory_order_acquire – в
ветви, где мы возвращаемся в начало цикла. Однако даже это перебор –
лишь один поток выполняет delete (тот, что сбросил счетчик в нуль),
поэтому только этому потоку нужно выполнить операцию захвата.
К счастью, поскольку fetch_add() – операция чтения-модификации-
записи, то она составляет часть последовательности освобождений, поэ-
тому для достижения цели нам достаточно дополнительной операции
load(). Если в ветви, где происходит возврат в начало цикла, счетчик
ссылок уменьшается до нуля, то здесь же можно перезагрузить счетчик
ссылок с семантикой std::memory_order_acquire, чтобы обеспечить
требуемое отношение синхронизируется-с, а в самой операции fetch_
add() достаточно задать std::memory_order_relaxed.


Как я понял, только один поток, который успел обнулить счётчик, должен
выполнить операцию delete для переменной ptr (76 и 84 строчки кода).
Чтобы этого добиться, необходимо, чтобы обе операции fetch_add имели
отношение синхронизирован-с, но использовать семантику std::memory_order_acquire
излишне, так как можно ограничится операцией захвата.

В данном моменте у меня возникла проблема в понимании этого утверждения, так как
автор использует в самой операции fetch_add() семантику std::memory_order_relaxed, а
только потом в самом ветвлении операцию ptr->internal_count.load(std::memory_order_acquire);

Из раздела 5.3.4. - Последовательности освобождений и отношение синхронизируется-с - привожу цитату, которая
описывает ситуацию, когда цепочка операций составляет последовательность
освобождений:
Кликните здесь для просмотра всего текста

"
операция сохранения помечена одним из признаков
memory_order_release, memory_order_acq_rel или memory_order_
seq_cst, а операция загрузки – одним из признаков memory_order_
consume, memory_order_acquire или memory_order_seq_cst
"
В таком случае "первая в ней операция сохранения синхронизирует-
ся-с (в случае memory_order_acquire или memory_order_seq_cst)
или предшествует-по-зависимости (в случае memory_order_consume)
последней операции загрузки".


Будет предпочтительнее, если вы ответите на вопросы по каждому пункту:
1) В моём понимании, автор добивается синхронизации и предоставленный код
C++
1
2
3
4
5
6
            else if(ptr->internal_count.fetch_add(
                        -1,std::memory_order_relaxed)==1)
            {
                ptr->internal_count.load(std::memory_order_acquire);
                delete ptr;
            }
эквивалентен этому
C++
1
2
3
4
5
            else if(ptr->internal_count.fetch_add(
                        -1,std::memory_order_acquire)==1)
            {
                delete ptr;
            }
Действительно ли это так?
2) В дополнение к предыдущему пункту - Как тогда в коде автора происходит
участие последней операции fetch_add() с семантикой std::memory_order_relaxed
в последовательности освобождений (и операцией fetch_add() в ветвлении, где возвращается результат),
если это противоречит тому, что было написано в разделе 5.3.4?

std::memory_order_relaxed не позволит операции сохранения иметь отношение синхронизируется-с
последней операции загрузки и гипотетически возможен случай, что оба потока попытаются очистить одну
область памяти, так как internal_count не успевает обновиться.
0
cpp_developer
Эксперт
20123 / 5690 / 1417
Регистрация: 09.04.2010
Сообщений: 22,546
Блог
04.07.2022, 15:12
Ответы с готовыми решениями:

Недостаточно памяти стеку протоколов Tcp/ip
Господа гуру! Кто может подсказать причину возникновения ошибки - &quot;Недостаточно памяти стеку...

Применение модели потенциальной ямы
Здравствуйте! Подскажите, пожалуйста, где возможно применение модели одномерной глубокой...

Применение кратной детерминированной модели
Доброе время суток . Нужно составить небольшой пример где будет разобрано применение кратной...

Применение С++ модели (QObjectList) вместе с Qt Quick
В Examples есть замечательный пример в папке: Qt\Examples\Qt-5.7\quick\models\objectlistmodel...

4
6340 / 3511 / 1427
Регистрация: 07.02.2019
Сообщений: 8,977
04.07.2022, 23:41 2
Лучший ответ Сообщение было отмечено Sidr как решение

Решение

Цитата Сообщение от Sidr Посмотреть сообщение
Будет предпочтительнее, если вы ответите на вопросы по каждому пункту:
1) Да.
2) Нет никакого противоречия. Я главу не помню, а искать лень, но в книге было подробно про транзитивные свойства read-modify-write операций (fetch_add к ним относится). Т.е. операцияptr->internal_count.fetch_add(-1,std::memory_order_relaxed) является частью последовательности освобождения операции ptr->internal_count.fetch_add(count_increase,std::memory_order_release) (ссылка на стандарт).
Проще говоря, в случае, если значение до декремента было 1, то это последняя rmw операция, а значит можно поставить барьер и вызвать деструктор.
1
1 / 1 / 0
Регистрация: 08.08.2021
Сообщений: 18
05.07.2022, 10:59  [ТС] 3
Цитата Сообщение от zayats80888 Посмотреть сообщение
Проще говоря, в случае, если значение до декремента было 1, то это последняя rmw операция, а значит можно поставить барьер и вызвать деструктор.
Очень хороший ответ на вопрос, спасибо большое!

Нам необходимо по словам автора предотвратить гонку за данными, и для этого мы должны гарантировать, что swap() происходит-раньше delete.
Я понял, что барьер ptr->internal_count.load(std::memory_order_acquire); не нужен для последней rmw операции (нужен для другой цели), так как срабатывает исправно даже с расслабленной моделью памяти.
Обнуление счётчика уже является гарантией того, что только один поток может вызвать операцию delete, так как декремент атомарен - это легко понять - но это не есть цель автора.

Если не окажется затруднительным, помогите, пожалуйста, ответить на эти вопросы:

1) Тогда с какой целью нужно ставить Как именно этот барьер даёт необходимую гарантию того, что swap() происходит раньше операции delete?
2) Разве не должно использоваться отношение происходит-раньше между swap() и delete (для реализации которого используется цикл), вместо отношения синхронизируется-с?
0
6340 / 3511 / 1427
Регистрация: 07.02.2019
Сообщений: 8,977
05.07.2022, 14:52 4
Лучший ответ Сообщение было отмечено Sidr как решение

Решение

Цитата Сообщение от Sidr Посмотреть сообщение
Я понял, что барьер ptr->internal_count.load(std::memory_order_acquire); не нужен для последней rmw операции (нужен для другой цели), так как срабатывает исправно даже с расслабленной моделью памяти.
Нет, этот барьер как раз и нужен только для последеней rmw операции, т.к. именно она и должна синхронизироваться. То, что у вас по вашему мнению работает исправно - ничего не значит. Чтобы увидеть UB, нужна архитектура с поддержкой relaxed семантики и определенная доля "удачи". На x86-64, например, все relaxed операции на самом деле имеют семантику acquire-release.
Цитата Сообщение от Sidr Посмотреть сообщение
Обнуление счётчика уже является гарантией того, что только один поток может вызвать операцию delete, так как декремент атомарен - это легко понять - но это не есть цель автора.
Суть барьера не в том, чтобы только один поток выплнил delete. Барьеры упорядочивают операции вокруг атомарных операций. В данном конкретном случае барьер гарантирует, что при delete в деструкторе узла будет уничтожен актуальный shared_ptr на данные, т.е. межпоточно упорядочивает swap и delete.
Т.е. переходя к вашим пунктам вопросам:
1) ptr->internal_count.load(std::memory_order_acquire)(операция С) транзитивно через ptr->internal_count.fetch_add(-1,std::memory_order_relaxed)(операция B) синхронизируется-с ptr->internal_count.fetch_add(count_increase,std::memory_order_release)(операция А). В даннм случае проверять значение после загрузки не нужно, т.к. оно гарантированно будет равно 0. Никто между декрементом и загрузкой не "влезет".
2) Т.к. swap происходит-раньше операции A, операция С происходит-раньше delete, A синхронизируется-с С(а значит A происходит-раньше С) - следовательно swap происходит-раньше delete.
1
1 / 1 / 0
Регистрация: 08.08.2021
Сообщений: 18
05.07.2022, 16:27  [ТС] 5
zayats80888, браво! Большое спасибо! Вы действительно очень сильно мне помогли понять это!
1
05.07.2022, 16:27
raxper
Эксперт
30234 / 6612 / 1498
Регистрация: 28.12.2010
Сообщений: 21,154
Блог
05.07.2022, 16:27
Помогаю со студенческими работами здесь

Применение объектно-ориентированной модели программирования
Применение объектно-ориентированной модели программирования Объект: Отрезок Количество объектов:...

Применение анимации к модели, созданной в 3Dmax
Сделал анимацию в 3д максе,создал эту же модель, как применить в юнити анимацию к моей модели...

Применение проверки достоверности к классу модели созданной entity framework
не могу понять почему не работает.Я его прописал в моделе сформированной entity framework ...

Применение Динамического выделения памяти
Надо решить задачу написав функцию. Нужно выделить память использую malloc. Помогите Плиз..!...

Работа кода из листинга из книги Энтони Уильямса
Всем привет, пытаюсь понять довольно сложную для меня тему, а именно барьеры памяти. И вроде...

Поиск картинки по свободному полю со списком
Мучаюсь третий день,никак не могу сообразить.Может местные асы помогут. Суть такая есть таблица с...

Запись к свободному сотруднику в свободное время
Добрый вечер! Есть база данных, в которой есть таблица с сотрудниками, их графиком работы (он у...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
5
Ответ Создать тему
Опции темы

Новые блоги и статьи
Счётчик на базе сумматоров + регистров и генератора сигналов согласования.
Hrethgir 07.01.2025
Создан с целью проверки скорости асинхронной логики: ранее описанного сумматора и предополагаемых fast регистров. Регистры созданы на базе ранее описанного, предполагаемого fast триггера. То-есть. . .
Как перейти с Options API на Composition API в Vue.js
BasicMan 06.01.2025
Почему переход на Composition API актуален В мире современной веб-разработки фреймворк Vue. js продолжает эволюционировать, предлагая разработчикам все более совершенные инструменты для создания. . .
Архитектура современных процессоров
inter-admin 06.01.2025
Процессор (центральный процессор, ЦП) является основным вычислительным устройством компьютера, которое выполняет обработку данных и управляет работой всех остальных компонентов системы. Архитектура. . .
История создания реляционной модели баз данных, правила Кодда
Programming 06.01.2025
Предпосылки создания реляционной модели В конце 1960-х годов компьютерная индустрия столкнулась с серьезными проблемами в области управления данными. Существовавшие на тот момент модели данных -. . .
Полезные поделки на Arduino, которые можно сделать самому
raxper 06.01.2025
Arduino как платформа для творчества Arduino представляет собой удивительную платформу для технического творчества, которая открывает безграничные возможности для создания уникальных проектов. Эта. . .
Подборка решений задач на Python
IT_Exp 06.01.2025
Целью данной подборки является предоставление возможности ознакомиться с различными задачами и их решениями на Python, что может быть полезно как для начинающих, так и для опытных программистов. . . .
С чего начать программировать микроконтроллер­­ы
raxper 06.01.2025
Введение в мир микроконтроллеров Микроконтроллеры стали неотъемлемой частью современного мира, окружая нас повсюду: от простых бытовых приборов до сложных промышленных систем. Эти маленькие. . .
Из чего собрать игровой компьютер
inter-admin 06.01.2025
Сборка игрового компьютера требует особого внимания к выбору комплектующих и их совместимости. Правильно собранный игровой ПК не только обеспечивает комфортный геймплей в современных играх, но и. . .
Обновление сайта www.historian.b­y
Reglage 05.01.2025
Обещал подвести итоги 2024 года для сайта. Однако начну с того, что изменилось за неделю. Добавил краткий урок по последовательности действий при анализе вредоносных файлов и значительно улучшил урок. . .
Как использовать GraphQL в C# с HotChocolate
Programming 05.01.2025
GraphQL — это современный подход к разработке API, который позволяет клиентам запрашивать только те данные, которые им необходимы. Это делает взаимодействие с API более гибким и эффективным по. . .
Модель полного двоичного сумматора с помощью логических операций (python)
AlexSky-coder 04.01.2025
def binSum(x:list, y:list): s=^y] p=x and y for i in range(1,len(x)): s. append((x^y)^p) p=(x and y)or(p and (x or y)) return s x=list() y=list()
Это мы не проходили, это нам не задавали...(аси­­­­­­­­­­­­­­­­­­­­­­­­­­х­р­о­н­­н­­­ы­­й счётчик с управляющим сигналом зад
Hrethgir 04.01.2025
Асинхронный счётчик на сумматорах (шестиразрядный по числу диодов на плате, но наверное разрядов будет больше - восемь или шестнадцать, а диоды на старшие), так как триггеры прошли тестирование и. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru