С Новым годом! Форум программистов, компьютерный форум, киберфорум
C++
Войти
Регистрация
Восстановить пароль
Карта форума Темы раздела Блоги Сообщество Поиск Заказать работу  
 
Рейтинг 4.73/15: Рейтинг темы: голосов - 15, средняя оценка - 4.73
1 / 1 / 0
Регистрация: 08.08.2021
Сообщений: 18
1

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

04.07.2022, 15:12. Показов 2752. Ответов 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
Programming
Эксперт
9485 / 562 / 19
Регистрация: 12.04.2006
Сообщений: 11,671
Блог
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
cpp_developer
Эксперт
20123 / 5690 / 417
Регистрация: 09.04.2010
Сообщений: 12,546
Блог
05.07.2022, 16:27
Помогаю со студенческими работами здесь

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

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

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

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

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

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

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


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

Или воспользуйтесь поиском по форуму:
5
Ответ Создать тему
Блоги программистов
Обновление сайта 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
Асинхронный счётчик на сумматорах (шестиразрядный по числу диодов на плате, но наверное разрядов будет больше - восемь или шестнадцать, а диоды на старшие), так как триггеры прошли тестирование и. . .
Руководство по созданию бота для Телеграм на Python
IT_Exp 04.01.2025
Боты для Телеграм представляют собой автоматизированные программы, которые выполняют различные задачи, взаимодействуя с пользователями через интерфейс мессенджера. В данной статье мы рассмотрим,. . .
Применение компонентов PrimeVue в Vue.js 3 на TypeScript
BasicMan 04.01.2025
Введение в PrimeVue и настройка окружения PrimeVue представляет собой мощную библиотеку компонентов пользовательского интерфейса для Vue. js 3, которая предоставляет разработчикам богатый набор. . .
Как стать Senior developer
cpp_developer 04.01.2025
В современной индустрии разработки программного обеспечения позиция Senior Developer представляет собой не просто следующую ступень карьерной лестницы, а качественно новый уровень профессионального. . .
Что известно о дате выхода Windows 12 и чего от нее ждать
IT_Exp 04.01.2025
В мире технологий постоянно происходят изменения, и операционные системы не являются исключением. Windows 11, выпущенная в октябре 2021 года, принесла множество инноваций и улучшений, но. . .
Что новенького в .NET Core 9
Programming 04.01.2025
Обзор ключевых изменений в . NET Core 9 Платформа . NET Core продолжает активно развиваться, и версия 9 представляет собой значительный шаг вперед в эволюции этой технологии. Новый релиз. . .
Инструкция по установке python3.13.1 в Debian 12
AlexSky-coder 03.01.2025
sudo apt update sudo apt install build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libreadline-dev libffi-dev wget. . .
Затестил триггеры. архив проекта прилагаю с GOA файлами в настройках архиватора проектов.
Hrethgir 03.01.2025
В этот раз нет закольцованности, потому что от неё только глюки, как я понял, логика не вырезанная. Триггеры очень быстрые если верить измерениям с помощью анализатора от Gowin. Есть ещё регистры,. . .
Python в помощь DevOps
IT_Exp 03.01.2025
Причины использования Python в работе DevOps Python стал неотъемлемой частью мира DevOps, и это не случайно. Этот язык программирования обладает множеством преимуществ, которые делают его. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru