Форум программистов, компьютерный форум, киберфорум
C++
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск  
 
 
0 / 0 / 0
Регистрация: 04.06.2022
Сообщений: 24

Вопрос по многопоточности

18.07.2025, 22:33. Показов 18059. Ответов 104

Студворк — интернет-сервис помощи студентам
Здравствуйте, сейчас смотрю книги по многопоточности, возникло несколько вопросов. Почему-то слово "вопрос" нельзя полностью написать в заголовке.

1. Известно, что переменную bool (или int, неважно) может 1 раз записать только 1 поток, остальные только читают, зачем тогда делать её atomic?

2. Улетят ли вызовы notify_one/notify_all вникуда, если они много раз вызваны перед методами, которые ожидают cv?

3. Допустим, есть 5 потоков и есть общий вектор с огромным количеством элементов. Первый поток изменяет только элементы с идексами 0, 5, 10; второй поток - элементы с индексами 1, 6, 11; третий поток - элементы с индексами 2, 7, 12 и т.д. Правильно ли я понимаю, что переброска кэша и связанное с ним замедление программы все равно может происходить, потому что индексы, с которыми работает каждый поток, находятся по соседству?

4. Вот пример потокобезопасной очереди из книги Вилльямса. Зачем при сравнении head с tail в функции get_tail мы используем мьютекс, который тут же перестает блокироваться после того, как мы вышли из функции?

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
#include <iostream>
#include <fstream>
#include <ios>
#include <string>
#include <numeric>
#include <map>
#include <vector>
#include <queue>
#include <iterator>
#include <memory>
#include <mutex>
#include <type_traits>
#include <algorithm>
#include <atomic>
#include <thread>
#include <chrono>
#include <cassert>
 
// clang -std=c++2a -m64 -o thread_safe_queue.exe thread_safe_queue.cpp
 
template<typename T> class thread_safe_queue {
private:
    struct node {
        std::shared_ptr<T> data;
        std::unique_ptr<node> next;
    };
    std::unique_ptr<node> head;
    node* tail;
 
    mutable std::mutex m_head;
    mutable std::mutex m_tail;
 
public:
    thread_safe_queue(const thread_safe_queue& ) = delete;
    thread_safe_queue() : head(new node()), tail(head.get()) {}
 
    node* get_tail() const {
        std::lock_guard<std::mutex> lg_tail(m_tail);
        return tail;
    }
 
    bool empty() const {
        std::lock_guard<std::mutex> lg_head(m_head);
        std::lock_guard<std::mutex> lg_tail(m_tail);
        return (head.get() == tail);
    }
 
    std::shared_ptr<T> pop() {
        std::lock_guard<std::mutex> lg_head(m_head);
        if (head.get() == get_tail()) { // why ?
            return std::shared_ptr<T>();
        }
 
        std::shared_ptr<T> res(head->data);
        std::unique_ptr<node> old_head = std::move(head);
        head = std::move(old_head->next);
        return res;
    }
 
    void push(const T new_value) {
        std::unique_ptr<node> q(new node);
        std::lock_guard<std::mutex> lg_tail(m_tail);
        tail->data = std::make_shared<T>(std::move(new_value));
        tail->next = std::move(q);
        tail = tail->next.get();
    }
};
 
int main(int argc, char *argv[]) {
    return 0;
}
0
cpp_developer
Эксперт
20123 / 5690 / 1417
Регистрация: 09.04.2010
Сообщений: 22,546
Блог
18.07.2025, 22:33
Ответы с готовыми решениями:

Соотношение многопоточности приложения c++ и многопоточности на уровне системы?
Возник следующий вопрос: в C++ существует два варианта работы с многопоточностью - std::theard и...

Управление потоками в многопоточности
вопрос простой: что посоветуете почитать по теме для начинающего? с помощью чего проще...

Нужна информация о многопоточности
дайте хорошую статью про создание многопоточных приложений...

104
 Аватар для zayats80888
6352 / 3523 / 1428
Регистрация: 07.02.2019
Сообщений: 8,995
23.07.2025, 13:56
Студворк — интернет-сервис помощи студентам
Цитата Сообщение от cdcodecpp Посмотреть сообщение
Скажите, пожалуйста, правильно я понимаю, что достаточно просто знать, что возникает UB не только когда 2 и больше потока пытаются переписать одно и то же значение, но и когда один читает, а второй пишет? Этого достаточно в виде "леммы" для понимания?
Да, пока достаточно.
Кликните здесь для просмотра всего текста
А тут я вам приведу цитату из стандарта в своем вольном переводе:
[intro.races-17]

Two actions are potentially concurrent if
- they are performed by different threads, or
- they are unsequenced, at least one is performed by a signal handler, and they are not both performed by the same signal handler invocation.
The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other, except for the special case for signal handlers described below. Any such data race results in undefined behavior.

Два действия являются потенциально конкурентными, если
- они выполняются разными потоками, или
- они не упорядочены, по крайней мере, одно из них выполняется обработчиком сигналов, и оба они не выполняются одним и тем же вызовом обработчика сигналов.
Выполнение программы содержит гонку данных, если она содержит два потенциально конкурентных действия, по крайней мере одно из которых не является атомарным, и ни одно из них не произошло раньше другого, за исключением особого случая для обработчиков сигналов, описанного ниже. Любая такая гонка данных приводит к неопределенному поведению.

Цитата Сообщение от cdcodecpp Посмотреть сообщение
А если бы команды не переупорядочивались компилятором и процессором, то всё равно было бы UB?
Странный вопрос, наличие UB заявляется в тексте стандарта. "Что там было бы написано, если бы не ..." я судить не берусь да и смысла не вижу. Возможно смягчились бы некоторые формулировки...

Не по теме:

Цитата Сообщение от Igor3D Посмотреть сообщение
Хочу услышать Ваше мнение по поводу UB при "одновременном" чтении/записи
По вашему это какое-то особенное UB? Оно отличается от любого другого?
Цитата Сообщение от Igor3D Посмотреть сообщение
Но у меня другой подход. Вот прямолинейное, лобовое решение (тот же поиск макс). Что здесь плохо?
Какой?
Нажимание рандомных клавиш на клавиатуре в попытке написать работающую программу?
Или вы все же сначала теорию изучаете?
Откуда вы вообще про какой-то поиск узнали и какие буквы нужно жмакать на клавиатуре?
Почему вдруг на определенном этапе обучения вы решили "нахрен эту теорию, я все знаю, мне все понятно, ничего вы мне не докажете"?
Цитата Сообщение от Igor3D Посмотреть сообщение
Напр ТС не упомянул что параллельный поиск макс на практике неэффективен
А почему ТС вообще должен был об этом упомянуть? 00

1
0 / 0 / 0
Регистрация: 04.06.2022
Сообщений: 24
23.07.2025, 16:43  [ТС]
Цитата Сообщение от zayats80888 Посмотреть сообщение
Да, пока достаточно.
То есть, если в get_tail нет мьютекса m_tail, то пока push обрабатывает tail, он может из-за оптимизаций компилятора и процессора попереставлять строки инструкции, из-за чего get_tail в pop может вернуть не старое или новое значение, а что-то, совсем не имеющее отношение к делу, именно поэтому надо прочесть значение или раньше, или позже push, и совсем не важно, прочёл pop старое значение и вернул NULL или прочёл новое значение и вернул какой-то результат. Главное, что pop прочел результат или строго до push, или строго после push, тогда он не получит какой-то кривой результат, не имеющий отношение ни к старому, ни к новому tail. Правильно?
А если вместо pop и push методы f1 и f2, и только 2 потока, первый поток вызывают f1, а второй f2, то нет никакой конкуренции и мьютексы уже не нужны, правильно (каждый метод вызывается не более чем одним потоком)?

C++
1
2
3
4
5
6
7
8
9
    
    void f1(node* ptr) {
        std::unique_ptr<node> q(ptr);
        tail->next = std::move(q);
    }
 
    void f2(const T new_value) {
        tail->data = std::make_shared<T>(std::move(new_value));
    }
0
 Аватар для zayats80888
6352 / 3523 / 1428
Регистрация: 07.02.2019
Сообщений: 8,995
23.07.2025, 17:11
Цитата Сообщение от cdcodecpp Посмотреть сообщение
Правильно?
Да, примерно так.
Цитата Сообщение от cdcodecpp Посмотреть сообщение
А если вместо pop и push методы f1 и f2, и только 2 потока, первый поток вызывают f1, а второй f2, то нет никакой конкуренции и мьютексы уже не нужны, правильно (каждый метод вызывается не более чем одним потоком)?
Да, но это бессмысленные действия...
1
0 / 0 / 0
Регистрация: 04.06.2022
Сообщений: 24
23.07.2025, 17:23  [ТС]
Цитата Сообщение от zayats80888 Посмотреть сообщение
Да, но это бессмысленные действия...
Это для понимания.
Спасибо за объяснения и комментарии, кажется, почти все понял в этом вопросе.
А если в очереди уже есть несколько элементов (например, добавились в конструкторе), а из метода push удалили последнюю строку tail = tail->next.get(); , то всё равно в get_tail нужен мьютекс? Строки

C++
1
2
3
4
5
6
7
void push(const T new_value) {
        std::unique_ptr<node> q(new node);
        std::lock_guard<std::mutex> lg_tail(m_tail);
        tail->data = std::make_shared<T>(std::move(new_value));
        tail->next = std::move(q);
        // tail = tail->next.get(); // убрали
}
всё равно могут привести к тому, что куча команд и инструкций из-за оптимизации процессора и компилятора может поменяться местами? Ведь меняется не tail, а переменные, находящиеся по другим адресам. Мне кажется, что мьютекс в get_tail уже не будет нужен, это правильное утверждение?
0
 Аватар для zayats80888
6352 / 3523 / 1428
Регистрация: 07.02.2019
Сообщений: 8,995
23.07.2025, 17:33
Цитата Сообщение от cdcodecpp Посмотреть сообщение
Ведь меняется не tail, а переменные, находящиеся по другим адресам.
Дело в том, что эти переменные в любом случае будут "читаться" при вызове деструктора очереди и вы обязаны будуте так или иначе упорчядочить это событие с событием их изменения.

Это безотносительно мьютекса хвоста очереди, просто вам к размышлению.
0
1977 / 833 / 115
Регистрация: 01.10.2012
Сообщений: 5,117
Записей в блоге: 2
23.07.2025, 23:58
Цитата Сообщение от cdcodecpp Посмотреть сообщение
он может из-за оптимизаций компилятора и процессора попереставлять строки инструкции, из-за чего get_tail в pop может вернуть не старое или новое значение, а что-то, совсем не имеющее отношение к делу,
Нет, конкретно для типа "указатель" мусора не будет, вот для структуры сплошь и рядом, половина "новая", другая "старая" если не засисяться. Здесь все просто: одновременные чтение/запись "считаются ошибкой" автоматом, во всяком случае утилитами/диагностикой. И не помешает копнуть
C++
1
2
std::lock_guard<std::mutex> lg_tail(m_tail);
tail->data = ..
А что будет если умный процессор/компилятор "возьмет и переставит"? Присваивание будет уже не под локом, все сразу рухнет. Выходит никто не переставляет?
Цитата Сообщение от cdcodecpp Посмотреть сообщение
кажется, почти все понял в этом вопросе.
0
24.07.2025, 15:00

Не по теме:

Цитата Сообщение от Igor3D Посмотреть сообщение
Нет, конкретно для типа "указатель" мусора не будет
Вы, конечно же, не будете писать многопоточный код для существующих платформ с неатомарным указателем.
Но если стандарт не гарантирует этого, то и вы не можете, пока речь не заходит о конкретном компиляторе с конкретными опциями для конкретной платформы. Правда, это уже не имеет отношения к языку.
Цитата Сообщение от Igor3D Посмотреть сообщение
А что будет если умный процессор/компилятор "возьмет и переставит"?
А вам не приходило в голову, что операция взятия блокировки мьютекса обладает особенной семантикой(о чем я тут не раз упоминал), и запрещает определенные перестановки?

0
1977 / 833 / 115
Регистрация: 01.10.2012
Сообщений: 5,117
Записей в блоге: 2
24.07.2025, 19:17
Цитата Сообщение от zayats80888 Посмотреть сообщение
Вы, конечно же, не будете писать многопоточный код для существующих платформ с неатомарным указателем.
Я не буду ставить такой задачи пока не возникнет необходимость, считаю это напрасным разбазариванием времени/энергии программиста. Пока это надо лишь "иметь ввиду".
Цитата Сообщение от zayats80888 Посмотреть сообщение
А вам не приходило в голову, что операция взятия блокировки мьютекса обладает особенной семантикой(о чем я тут не раз упоминал), и запрещает определенные перестановки?
Про мутекс ничего толком не знаю, наверно обладает, ведь фактов такой перестановки нет. К тому же о каком мутексе идет речь? Реализации разные, в том числе самопальные, напр я почти всегда использую unfair mutex на atomic. А вот атомик точно обладает (см картинку с листингом 5.2 выше), ничто не может быть переставлено/помещено после атомарной записи (аналогично до атомарного чтения). Правда это всего лишь одна "модель памяти" (если я верно употребляю этот термин) по умолчанию. И с точки зрения производительности - не лучшая. И есть еще много других, но бросаться в такую пучину - та ну нафиг

Не по теме:

Кстати в примере с поиском макс ожидал от Вас варианта 4. Но увы.. :(

0
 Аватар для zayats80888
6352 / 3523 / 1428
Регистрация: 07.02.2019
Сообщений: 8,995
24.07.2025, 21:06

Не по теме:

Цитата Сообщение от Igor3D Посмотреть сообщение
Я не буду ставить такой задачи пока не возникнет необходимость, считаю это напрасным разбазариванием времени/энергии программиста
Так её и не нужно ставить, если сразу писать корректный код, тем более это не требует каких-то дополнительных усилий.
А отладку и поиск багов в говнокоде вы не считаете напрасным разбазариванием?
Цитата Сообщение от Igor3D Посмотреть сообщение
К тому же о каком мутексе идет речь? Реализации разные
У вас там std::mutex, нет необходимости знать его реализацию, все контракты прописаны в стандарте.
Да и все реализации, написанные на с++ должны соответсвовать стандарту.
И чего это вдруг вы про реализации заговорили?
Цитата Сообщение от Igor3D Посмотреть сообщение
я почти всегда использую unfair mutex на atomic
Занимаетесь бесполезным сжиганием электроэнергии? :)))
Реализацией не поделитесь?
Цитата Сообщение от Igor3D Посмотреть сообщение
И есть еще много других, но бросаться в такую пучину - та ну нафиг
Так много? У вас на одной руке пальцев меньше, тоже мне пучина :)



Добавлено через 7 минут

Не по теме:

Цитата Сообщение от Igor3D Посмотреть сообщение
Кстати в примере с поиском макс ожидал от Вас варианта 4.
Во-первых, я не понимаю, что там написано, но, там точно нехватает Unlock().
Во-вторых, я не знаком с omp, но распараллеливать поиск по блокам памяти, меньшим чем самый жирный кэш актульных процессоров я бы точно не стал.

0
1977 / 833 / 115
Регистрация: 01.10.2012
Сообщений: 5,117
Записей в блоге: 2
24.07.2025, 21:56
Цитата Сообщение от zayats80888 Посмотреть сообщение
Занимаетесь бесполезным сжиганием электроэнергии? ))
Реализацией не поделитесь?
Занимаюсь. Долгое время юзал свой (атомарный) ReadWriteLocker, но потом все-таки взял из TBB. Спиннинг эффективнее, учитывает специфику процессоров, и вообще - длинно, нудно и никакого творчества
Цитата Сообщение от zayats80888 Посмотреть сообщение
Во-вторых, я не знаком с omp, но распараллеливать поиск по блокам памяти, меньшим чем самый жирный кэш актульных процессоров я бы точно не стал.
Да, пример синтетический, хотелось посмотреть реакцию ТС
Цитата Сообщение от zayats80888 Посмотреть сообщение
я не понимаю, что там написано, но, там точно нехватает Unlock().
Конечно имелся ввиду scoped, хорошо, пусть будет std::
C++
1
2
3
4
5
6
7
8
9
10
11
auto theMax = data[0];
std::mutex m1;
 
#pragma omp parallel for
for (int i = 0; i < count; ++i) {
 if (data[i] > theMax) {
  std::lock_guard<std::mutex> lock(m1);
  if (data[i] > theMax) 
   theMax = data[i];
 } 
}
В OpenMP можно решить это директивой, но я не об этом спрашиваю.

Цитата Сообщение от zayats80888 Посмотреть сообщение
..если сразу писать корректный код, тем более это не требует каких-то дополнительных усилий.
Такое утверждение мне кажется слишком смелым. Ну вот когда-то написал человек код подобный примеру выше, чем он виноват? "Двойная проверка" - широко известный прием, код работает, и с ним намного быстрее. Можно конечно тупенько сделать theMax атомиком, но, как Вы понимаете, от лока это не избавляет
0
24.07.2025, 22:19

Не по теме:

Цитата Сообщение от Igor3D Посмотреть сообщение
Ну вот когда-то написал человек код подобный примеру выше, чем он виноват?
Тем, что код не соответствует стандарту, разве что тип элемента массива атомарный, но тогда это точно медленнее однопоточного варианта.
Цитата Сообщение от Igor3D Посмотреть сообщение
код работает, и с ним намного быстрее
У меня нет таких гарантий ("мамой клянусь" не считаются).
Цитата Сообщение от Igor3D Посмотреть сообщение
Можно конечно тупенько сделать theMax атомиком, но, как Вы понимаете, от лока это не избавляет
Про CAS не слышали?
Но, это в любом случае будет медленно в таком виде.

0
1977 / 833 / 115
Регистрация: 01.10.2012
Сообщений: 5,117
Записей в блоге: 2
26.07.2025, 17:03
Цитата Сообщение от zayats80888 Посмотреть сообщение
У меня нет таких гарантий ("мамой клянусь" не считаются).
"Гарантии дает только страховой полис". Я изучал эту ситуацию на Qt 4.5 с тамошними мутексами/атомиками. Вполне возможно с тех пор что-то изменилось, и мои выводы неточны/неверны, тем не менее я опираюсь на этот свой опыт.
Цитата Сообщение от zayats80888 Посмотреть сообщение
Про CAS не слышали?
Но, это в любом случае будет медленно в таком виде.
Ясно что получить хоть какую-то масштабируемость на нулевом кластере не удастся. Др словами расчет одной ниткой здесь практически всегда быстрее. Нужно решить др задачу: избежать разгрома на мутексе. Если просто убрать "некорректную" двойную проверку, каждая нитка хватает мутекс, и, особенно если ниток 8 и более, то провал ужасный: не просто "медленнее", а на порядок и более. Просто атомик я (в свое время) пробовал, скорость хуже, но не провально (не вдвое и даже не в полтора)

Думаю что правильно сделать theMax атомиком и использовать relaxed семантику, по крайней мере для первого чтения. Это выглядит как просто "чтение не атомика" (atomicity) из др нитки, накладных расходов быть не должно. В данном конкретном случае можно сделать еще лучше, да, пронести через CAS и обойтись без мутекса.
Зачем же надо было выполнять такую задачу всеми нитками?
Ну далеко не всегда мы хорошо знаем насколько трудоемка задача, поэтому если пре-проверка возможна - ее нужно использовать.

И да, я совсем не гордюсь тем что нарушаю стандарт и понимаю что "нарываться" не следует. Но в первую очередь надо заботиться о производительности и функционале, "а шуба подождет"
0
26.07.2025, 18:16

Не по теме:

Цитата Сообщение от Igor3D Посмотреть сообщение
В данном конкретном случае можно сделать еще лучше, да, пронести через CAS и обойтись без мутекса.
В данном конкретном случае можно было не "приклеивать крылья к кирпичу", а взять и целиком выкинуть однопоточный код, заменив на готовый библиотечный вызов функции, например, или написать самому, но нормально. Ну разбейте вы массив на те же 8 кусков и пусть каждый поток ищет свой локальный максимум. Ну нет тут необходимости синхронизации на каждой итерации, достаточно одной в конце. И при таком подходе уже не имеет значения, через атомик она или через мьютекс, т.к. время синхронизации крайне мало по отношению к времени полезной рааботы.

0
1977 / 833 / 115
Регистрация: 01.10.2012
Сообщений: 5,117
Записей в блоге: 2
27.07.2025, 14:21
Цитата Сообщение от zayats80888 Посмотреть сообщение
Ну разбейте вы массив на те же 8 кусков и пусть каждый поток ищет свой локальный максимум. Ну нет тут необходимости синхронизации на каждой итерации,
Рассматривать учебную задачу как реальную обычно не имеет смысла. С чисто практической, утилитарной точки зрения лучшая синхронизация - ее отсутствие. Надо делать так чтобы нитки не пересекались и каждая работала со своими данными. При этом да, часто надо заводить массивы вместо переменных. Все эти мутексы/шмутексы - не от хорошей жизни. Переоценивать их не стоит
Цитата Сообщение от zayats80888 Посмотреть сообщение
И это база языка, которую неплохо бы освоить.
Не считаю это такой уж "фундаментальной основой" которую каждый прямо-таки обязан знать. Не спорю, копаться в этом интересно (лично мне), но это скорее узкая/специфичная область, можно успешно юзать "многопоточность" и без этих тонкостей.
0
0 / 0 / 0
Регистрация: 04.06.2022
Сообщений: 24
01.08.2025, 15:50  [ТС]
5. Есть ли санитайзер для Windows, ищущий data race?

6. Иногда на Linux Address Sanitizer ловил ошибки, связанные с data race, которые должен ловить Thread Sanitizer. Правильно я понимаю, что нередко Address Sanitizer может поймать ошибки, связанные с многопоточностью (Thread Sanitizer) и что это не связанно с уникальностью тех учебных багов, с которыми я столкнулся?

7. В книге Вильямса есть пример очереди без блокировок, построенной на атомиках и подсчёте ссылок, когда push и pop из 5 строчек разбухают в 50. Часто ли встречаются на практике такие запутанные алгоритмы с подсчётом ссылок или этот код был показан как учебный пример?

8. Допустим, есть очередь, в которой есть задачи, которые надо распределять по потокам. Правильно я понимаю, что если время выполнения каждой такой задачи достаточно велико, то нет необходимости придумывать, как сделать очередь (или другую структуру) свободной от блокировок и от ожиданий? Правильно я понимаю, что необходимость кода, который свободен от блокировок и/или ожиданий возникает только тогда, когда задачи, которые выполняются каждым из потоков, выполняются за достаточно малое время и сопоставимы с временными затратами, которые расходуются на мьютексы и атомики?

9. Правильно я понимаю, что memory_order_seq_cst нужен либо для отладки кода, потому что самый простой и не допускает перестановку, либо для тех участков, которые не являются узким местом кода и нет смысла использовать memory_order_acquire / memory_order_release?

10. Правильно я понимаю, что memory_order_relaxed используется только в комбинации с memory_order_acquire / memory_order_release и / или memory_order_seq_cst и служит для оптимизации и ускорения кода? Сам по себе, без других семантик, он никакого смысла не несёт?

11. Вот пример кода для стэка без блокировок из книги Вилльямса.
Правильно я понимаю, что aquire / release нужны только для того, чтобы позволить процессору и компилятору сдвигать команды с целью оптимизации и ускорения и в тоже время быть уверенным, что всё, что было до release, там и останется, как и всё, что оказалось после aquire не сдвинется выше? Надо только, чтобы в процессе оптимизации, выполняемой компилятором и процессором, не произошло сдвига строчек кода (машинных команд) вниз (для release) или вверх (для aquire).

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
#include <memory>
#include <atomic>
#include <thread>
 
// clang -std=c++2a -m64 -o lock_free_stack.exe lock_free_stack.cpp
 
using namespace std;
 
template<typename T> class lock_free_stack {
private:
    struct node;
    struct counted_node_ptr {
        int external_count;
        node* ptr;
    };
    struct node {
        shared_ptr<T> data;
        atomic<int> internal_count;
        counted_node_ptr next;
        node(T const& data_): data(make_shared<T>(data_)), internal_count(0) {}
    };
 
    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, 
            memory_order_acquire, 
            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(memory_order_relaxed);
        while(!head.compare_exchange_weak(new_node.ptr->next, new_node, 
            memory_order_release, 
            memory_order_relaxed));
    }
 
    shared_ptr<T> pop() {
        counted_node_ptr old_head = head.load(memory_order_relaxed);
        for(;;) {
            increase_head_count(old_head);
            node* const ptr = old_head.ptr;
            if(!ptr) {
                return shared_ptr<T>();
            }
            if(head.compare_exchange_strong(old_head, ptr->next, memory_order_relaxed)) {
                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, memory_order_release) == -count_increase) {
                    delete ptr;
                }
                return res;
            }
            else if(ptr->internal_count.fetch_add(-1, memory_order_relaxed) == 1) {
                (void)ptr->internal_count.load(memory_order_acquire);
                delete ptr;
            }
        }
    }
};
 
int main() {
    lock_free_stack<int> s;
    s.push(1);
    s.push(2);
    (void)s.pop();
    return 0;
}
12. Правильно я понимаю, что acquire и release не должны обязательно вызываться в коде парами и друг за другом, что как вызов acquire, так и вызов release могут улететь вникуда, не дождавшись "дополняющего" вызова? Что метод push, содержащий release, может вызваться 100 раз, а метод pop, содержащий aquire, только 10. Как и наоборот: pop 100 (например, в конструкторе произошло добавление 1000 элементов без помощи метода push), а push 10. Если для конкретного aquire не обязательно должен быть конкретный release, а для конкретного release не обязательно должен быть конкретный aquire, то они нужны только для того, чтобы код ускорился за счёт более оптимального распределения команд (процессором и компилятором) по сравнению с seq_cst, не перемешав при этом эти самые команды для тех случаев, когда вызовы pop и push прекрылись во времени?
0
451 / 176 / 29
Регистрация: 12.12.2020
Сообщений: 1,367
01.08.2025, 16:07
Цитата Сообщение от cdcodecpp Посмотреть сообщение
Известно, что переменную bool (или int, неважно) может 1 раз записать только 1 поток, остальные только читают, зачем тогда делать её atomic?
переменная может писаться не за один такт а за несколько, например если это массив, или какая нить переменная четырехбайтная, или строка. И тогда есть вероятность что один поток запишет часть переменной, а потом второй прочитает ее всю и получится что прочел он мусор.
0
1977 / 833 / 115
Регистрация: 01.10.2012
Сообщений: 5,117
Записей в блоге: 2
02.08.2025, 01:02
Цитата Сообщение от cdcodecpp Посмотреть сообщение
8. Допустим, есть очередь, в которой есть задачи, которые надо распределять по потокам. Правильно я понимаю, что если время выполнения каждой такой задачи достаточно велико, то нет необходимости придумывать, как сделать очередь (или другую структуру) свободной от блокировок и от ожиданий? Правильно я понимаю, что необходимость кода, который свободен от блокировок и/или ожиданий возникает только тогда, когда задачи, которые выполняются каждым из потоков, выполняются за достаточно малое время и сопоставимы с временными затратами, которые расходуются на мьютексы и атомики?
Да, правильно. Если у Вас "хорошие" задачи, что обсчитываются хорошие доли секунды и более - то любая система работает хорошо. При большем времени выполнения возникают проблемы с гранулярностью и пере-распределением нагрузки, но это не смертельно. А вот когда задачи слишком мелкие - масштабировать их непросто а часто и вообще не удается. Заметим что OpenMP по умолчанию как бы "объединяет задачи в пачки", напр
C++
1
2
#pragma omp parallel for // default = static
for (int i = 0; i < 100; ++i)
Первые 25 задач выполнятся первой ниткой, вторые второй и.т.д. (если всего ниток 4)
Также есть проблема "бригады" ниток, ее запуска/готовности

В общем, "не берите тяжелого в руки и дурного в голову". Lock-free - это конечно очень интересное, заводное дело, но чисто практический результат ну.. скажем, выходит довольно скромным

Добавлено через 3 минуты
Цитата Сообщение от Alex1126 Посмотреть сообщение
Известно, что переменную bool (или int, неважно) может 1 раз записать только 1 поток, остальные только читают, зачем тогда делать её atomic?
Без atomic остальные могут прочитать старое значение, даже если запись (другим потоком) уже свершилась
0
112 / 110 / 30
Регистрация: 08.05.2021
Сообщений: 485
03.08.2025, 10:02
Цитата Сообщение от Igor3D Посмотреть сообщение
Lock-free - это конечно очень интересное, заводное дело, но чисто практический результат ну.. скажем, выходит довольно скромным
Не применимо в силу низкой квалификации и "довольно скромный практический результат" - это сильно разные вещи. Последнее просто оправдание своего низкого уровня, не более.

Цитата Сообщение от Igor3D Посмотреть сообщение
Без atomic остальные могут прочитать старое значение, даже если запись (другим потоком) уже свершилась
Неверно. Нигде не может прочитаться "старое" значение.

cdcodecpp, тему полностью не читал, но кажется ты пытаешься выработать для себя какие-то рецепты(паттерны) и далее писать код, просто комбинируя их. Вот, например:
Цитата Сообщение от cdcodecpp Посмотреть сообщение
9. Правильно я понимаю, что memory_order_seq_cst нужен либо для отладки кода, потому что самый простой и не допускает перестановку, либо для тех участков, которые не являются узким местом кода и нет смысла использовать memory_order_acquire / memory_order_release?
Цитата Сообщение от cdcodecpp Посмотреть сообщение
10. Правильно я понимаю, что memory_order_relaxed используется только в комбинации с memory_order_acquire / memory_order_release и / или memory_order_seq_cst и служит для оптимизации и ускорения кода? Сам по себе, без других семантик, он никакого смысла не несёт?
Остальное скорее всего в том же духе.

В общем - это не особо работает. Для того, чтобы понять какие-либо инструменты/средства, тебе нужно сначала понять проблемы, которые эти средства решают. Это как минимум. Зачастую ещё и контекст применения(не связанный непосредственно с решаемыми проблемами) этих инструментов/средств большое значение имеет.

Вот можешь для начала написать максимально просто - ни атомиков, ни мутексов и прочего. Дальше посмотри, работает ли оно, как работает и если не работает, то в чём причина. Далее поправляешь и повтор. Ну это если стоит цель научиться.
0
 Аватар для SmallEvil
4086 / 2975 / 813
Регистрация: 29.06.2020
Сообщений: 11,000
03.08.2025, 11:22
Цитата Сообщение от mashmed135 Посмотреть сообщение
написать максимально просто - ни атомиков, ни мутексов и прочего. Дальше посмотри
На что смотреть? На UB?
0
1977 / 833 / 115
Регистрация: 01.10.2012
Сообщений: 5,117
Записей в блоге: 2
03.08.2025, 19:13
Цитата Сообщение от mashmed135 Посмотреть сообщение
Неверно. Нигде не может прочитаться "старое" значение.
Почитайте тему и/или упомянутую книгу, потом будете давать советы (тыкать не нужно)
0
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
raxper
Эксперт
30234 / 6612 / 1498
Регистрация: 28.12.2010
Сообщений: 21,154
Блог
03.08.2025, 19:13

изучение многопоточности
с чего стоит начать изучение многопоточности? есть базовые знания по С++, основы ООП. пытался...

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

Менеджмент жесткого диска при многопоточности
Пусть у меня 4-ех ядерный процессор, и запущено 4 рабочих потока (в одном процессе). Казалось бы,...

Реализация многопоточности в консоли
Доброго времени суток. Не могу разобраться в многопоточности. Реализовано перемещение по меню с...

Сравнение многопоточности С++11 и WinAPI
У меня скорее теоретический вопрос, чем практический. Есть ли разница работы с многопоточностью в...


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

Или воспользуйтесь поиском по форуму:
40
Ответ Создать тему
Новые блоги и статьи
Нейтральные знания ..., ... чистая наука. Пока что-то проходит модерацию на Хабре, стоит развить мысль ...
Hrethgir 20.07.2026
К таким радикальным взглядам я конечно в той публикации не приходил, но чтобы скоротать вечер, решил углубиться немного. 1. Почему показания термометра заряжены целью? Цель заложена в самом. . .
Установка нескольких штампов электронной подписи в строго определенных местах файла docx
ВладимирСамохин 19.07.2026
(В!) Работа с Электронной подписью - это неотъемлемая часть современного документооборота. Но что делать, если нужно поставить несколько штампов электронной подписи в строго определенных местах. . .
сукцессия 35. Научная статья о проделанной работе
anaschu 19.07.2026
Написал в формате латекс и пдф
Вангую, что это не пройдёт модерацию, и на неделе я запущу свой сервер.
Hrethgir 19.07.2026
Эта публикация сейчас в песочнице и ждёт приглашения. https:/ / habr. com/ ru/ sandbox/ 295048/ По ссылке 403. Не очень информативно такую ссылку постить. Запись от Usaga размещена Сегодня в 06:46 . . .
сукцессия 33. открытые вопросы от клауде
anaschu 19.07.2026
"Что накопилось за эту часть А — тринадцать правок, из которых шесть пришли из ваших вопросов и каждая оказалась реальной ошибкой, а не калибровкой: односторонний симбиоз, отсутствующий листопад,. . .
32 сукцессия
anaschu 19.07.2026
сукцессия 28‑мерное ядро стабилизировано Коллеги, фиксирую разбор инженерных правок и их изоморфную проекцию на экономику, меметику и половой отбор. Модель теперь не «подкручивает» сходимость —. . .
сукцессия 31: модель микоризы - это модель ещё нескольких явлений, социальных и экономических
anaschu 18.07.2026
Теория «Всего»: апдейт v1. 1. 2 — 28‑мерное ядро стабилизировано Коллеги, фиксирую разбор инженерных правок и их изоморфную проекцию на экономику, меметику и половой отбор. Модель теперь не. . .
сукцессия 30. Массив проверяющих друг друга моделей
anaschu 18.07.2026
Архитектура сети взаимопроверяющих моделей микоризной сукцессии (v2. 0) Развитие тензорного ОДУ-ядра и создание кросс-платформенного калибровочного полигона Уважаемые коллеги! В продолжение. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru