Russian Qt Forum

Qt => Многопоточное программирование, процессы => Тема начата: Igors от Май 24, 2019, 07:11



Название: memory_order
Отправлено: Igors от Май 24, 2019, 07:11
Добрый день

Увидел эту (https://en.cppreference.com/w/cpp/atomic/memory_order) ссылку и решил "почитать" :)

Цитировать
Relaxed ordering

Atomic operations tagged memory_order_relaxed are not synchronization operations; they do not impose an order among concurrent memory accesses. They only guarantee atomicity and modification order consistency.
Ну вроде ясно - записанное данной ниткой значение видно ей самой, а для остальных ничего не гарантируется (если сами не позаботятся - могут и не увидеть новое значение). А под "atomicity" наверно имеется ввиду "атомарность присваивания/модификации" (т.е. только целиком, не по кускам)

Дальше там идет вполне понятный пример, а потом вот эта фраза - ее я уже не понял
Цитировать
Typical use for relaxed memory ordering is incrementing counters, such as the reference counters of std::shared_ptr, since this only requires atomicity, but not ordering or synchronization (note that decrementing the shared_ptr counters requires acquire-release synchronization with the destructor)
Это как же "not ordering"? Если одна нитка напр увеличила счетчик и он стал = 1, и эти изменения никому не видны, то другая, увидев старый 0, тоже может поставить 1. Или как?

Прошу пояснить. Спасибо


Название: Re: memory_order
Отправлено: Old от Май 24, 2019, 08:40
Ну вроде ясно - записанное данной ниткой значение видно ей самой, а для остальных ничего не гарантируется (если сами не позаботятся - могут и не увидеть новое значение). А под "atomicity" наверно имеется ввиду "атомарность присваивания/модификации" (т.е. только целиком, не по кускам)
Не ясно. :)
Модели памяти и ордеренги это больше про перестановки операций доступа к памяти, а не просто атомарность присвоения.
Relax говорит компилятору, что операцию можно перемешивать с другими операциями доступа к памяти как угодно, т.к. эта операция не используется для синхронизации между потоками, а просто изменят счетчик.


Название: Re: memory_order
Отправлено: Авварон от Май 24, 2019, 08:53
Relax говорит компилятору

процессору


Название: Re: memory_order
Отправлено: Old от Май 24, 2019, 09:23
процессору
Компилятору, точнее его оптимизатору, который и решает какие операции с какими можно переставить, а какие нельзя (и они должны выполняться строго в этой последовательности).


Название: Re: memory_order
Отправлено: Авварон от Май 24, 2019, 11:26
Компилятору, точнее его оптимизатору, который и решает какие операции с какими можно переставить, а какие нельзя (и они должны выполняться строго в этой последовательности).


facepalm.jpg

Вы можете скомпилировать программу, вставив барьер компилятора, глазками посмотреть что порядок инструкций правильный и все равно схлопотать реордер даже на x86 (https://preshing.com/20120515/memory-reordering-caught-in-the-act/).


Название: Re: memory_order
Отправлено: Old от Май 24, 2019, 12:01
Вы можете скомпилировать программу, вставив барьер компилятора, глазками посмотреть что порядок инструкций правильный и все равно схлопотать реордер даже на x86 (https://preshing.com/20120515/memory-reordering-caught-in-the-act/).
Хорошо, пусть будет так: ... говорит компилятору, который генерит код, который говорит процессору... :)


Название: Re: memory_order
Отправлено: Igors от Май 24, 2019, 12:48
Посмотрел еще раз пример

Цитировать
For example, with x and y initially zero,

// Thread 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// Thread 2:
r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D

is allowed to produce r1 == r2 == 42 because, although A is sequenced-before B within thread 1 and C is sequenced before D within thread 2, nothing prevents D from appearing before A in the modification order of y, and B from appearing before C in the modification order of x. The side-effect of D on y could be visible to the load A in thread 1 while the side effect of B on x could be visible to the load C in thread 2. In particular, this may occur if D is completed before C in thread 2, either due to compiler reordering or at runtime.
Т.к. никаких указаний/блокировок нет, то остается полагать что порядок выполнения AB / CD - любой. Но видимость в др. нитке срабатывает, то есть если Thread 1(B) прорвется первой и установит x - он будет видим в Thread 2. Оказывается если пишем relaxed - то записанное значение будет верно relaxed-прочитано в др нитке (конечно если оно не перекрыто опять). Однако про "всю память до того" ничего не говорится.

Разгорающаяся полемика о том кто там чего "переставляет" на мой взгляд не стоит свеч, т.к. это уже следствие "кто кого видит". Из последней фразы
Цитировать
either due to compiler reordering or at runtime
можно заключить что этим грязным делом занимаются все кому не лень  :)


Название: Re: memory_order
Отправлено: Авварон от Май 24, 2019, 13:12
Хорошо, пусть будет так: ... говорит компилятору, который генерит код, который говорит процессору... :)

Никто процессору ничего не говорит, он _сам_ выбирает порядок, в котором выполняются инструкции так, чтобы использовать ресурсы процессора по максимуму.
Если сейчас идет запись в кэш, то нет смысла выполнять следующую инструкцию записи, а можно выполнить инструкцию сложения или умножения.
Главное, чтобы не было зависимости по данным - если мы читаем ячейку Х и ее же складываем, то переупорядочить не выйдет. А если пишем в Х а складываем с Y, то не важно, в каком порядке выполнять инструкции.


Название: Re: memory_order
Отправлено: Авварон от Май 24, 2019, 13:16
можно заключить что этим грязным делом занимаются все кому не лень  :)

Этим занимается в основном ARM.
На x86 только один случай возможен, который и рассмотрен в статье:
Код:
Memory ordering on x86 is actually really strong compared to others.
You've picked the one example of when x86 //is// allowed to reorder memory operations: StoreLoad.

The rest of the cases {StoreStore, LoadLoad, LoadStore} are ordered.

Т.е. на арме достаточно двух переменных (нужно, чтобы не было зависимости по данным), чтобы поймать reorder, а на x86 надо аж 4. В более простых случаях такого не будет.


Название: Re: memory_order
Отправлено: Old от Май 24, 2019, 13:32
Никто процессору ничего не говорит, он _сам_ выбирает порядок, в котором выполняются инструкции так, чтобы использовать ресурсы процессора по максимуму.
Если сейчас идет запись в кэш, то нет смысла выполнять следующую инструкцию записи, а можно выполнить инструкцию сложения или умножения.
Главное, чтобы не было зависимости по данным - если мы читаем ячейку Х и ее же складываем, то переупорядочить не выйдет. А если пишем в Х а складываем с Y, то не важно, в каком порядке выполнять инструкции.
Да ладно. :)
Для организации барьеров (разговор с процессором) компилятор использует специальные инструкции. Для x86-64 это LOCK и MFENCE.
А по ссылке для всяких разных платформ: https://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html


Название: Re: memory_order
Отправлено: ViTech от Май 24, 2019, 13:51
Никто процессору ничего не говорит, он _сам_ выбирает порядок, в котором выполняются инструкции так, чтобы использовать ресурсы процессора по максимуму.

Всё-таки процессор выполняет программу, созданную компилятором. Так что изначально компилятор говорит процессору всё :), это уже потом процессор может своевольничать и переставлять команды по своему усмотрению. А все эти relaxed и прочие - это попытки со стороны компилятора заставить процессор выполнять инструкции в нужном порядке, насколько я понимаю.


Название: Re: memory_order
Отправлено: Авварон от Май 24, 2019, 14:29
Да ладно. :)

Собери пример из стати хоть ;) Посмотри в дизассемлер и скажи как же возможен случай 0, 0 на выходе без перестановки команд процессором.

Для организации барьеров (разговор с процессором) компилятор использует специальные инструкции. Для x86-64 это LOCK и MFENCE.

Компилятор сам ничего не делает, он просто собирает тот код, который написан в атомике=) А там уже ассемблерные вставки в зависмоcти от флажков.
Да, барьер памяти - это специальная команда.
Вот только причем тут
Цитировать
Компилятору, точнее его оптимизатору, который и решает какие операции с какими можно переставить, а какие нельзя
Если кто не в курсе, то оптимизации делаются задолго до всяких MFENCE=) Они делаются на промежуточном представлении, чтобы не быть завязанным на конкретную архитектуру (точнее, делаются и там и там, но большинство - на промежуточном уровне)


Название: Re: memory_order
Отправлено: Old от Май 24, 2019, 14:32
Собери пример из стати хоть ;) Посмотри в дизассемлер и скажи как же возможен случай 0, 0 на выходе без перестановки команд процессором.
Мы что сейчас обсуждаем? :)
Вы не согласны, что компилятор может говорить процессору, как выполнять тот или иной блок команд? Так в вашей статье это разбирается.
С чем вы спорите? :)


Название: Re: memory_order
Отправлено: Авварон от Май 24, 2019, 14:36
С чем вы спорите? :)

С вашим утверждением, которое я процитировал - см выше.


Название: Re: memory_order
Отправлено: Old от Май 24, 2019, 14:38
С вашим утверждением, которое я процитировал - см выше.
Компилятор разбирает исходник и в зависимости от типа барьера может генерировать разный код для процессора. Вы не согласны? :)


Название: Re: memory_order
Отправлено: Авварон от Май 24, 2019, 14:41
Компилятор разбирает исходник и в зависимости от типа барьера может генерировать разный код для процессора. Вы не согласны? :)
Может. И делает. Вы же утверждаете, что реордер происходит только из-за того, что компилятор там что-то соптимизировал и переставил. Это неправда.
Еще раз:
Цитировать
Компилятору, точнее его оптимизатору, который и решает какие операции с какими можно переставить, а какие нельзя
Если бы была проблема в компиляторе, то проблемы бы не было - пишем везде volatile, компилируем без оптимизаций и вуаля. Ток чот это не помогает.


Название: Re: memory_order
Отправлено: Old от Май 24, 2019, 14:53
Может. И делает. Вы же утверждаете, что реордер происходит только из-за того, что компилятор там что-то соптимизировал и переставил. Это неправда.
Да где же я утверждал что "ТОЛЬКО"? :)
Перечитайте мое сообщение, на которое вы возразили и последнее на которое отвечаете сейчас. Я хотел сказать лишь это. :)


Название: Re: memory_order
Отправлено: Авварон от Май 24, 2019, 15:10
Перечитайте мое сообщение, на которое вы возразили и последнее на которое отвечаете сейчас. Я хотел сказать лишь это. :)

Слава богу, разобрались =)

Просто из ваших слов может создастся ложное впечатление, что если я руками напишу ассемблерную вставку, глазками проверю, что она правильно
скопилировалась и компилятор "не нахимичил" с перестановками, то моя программа будет работать так, как написано в коде - сначала инструкция А, потом инструкция Б. Это не так, процессор имеет право переставлять инструкции, если не сказать ему явно этого не делать специальной же инструкцией.

Компилятор конечно может посмотреть на ваш атомик и вставить эту инструкцию сам, но скорее всего, там просто внутри ассемблерная вставка, типа такой (https://github.com/qt/qtbase/blob/5.5/src/corelib/arch/qatomic_x86.h#L176). Потому что зачем хачить компилятор, если можно не хачить?
Возможно (!) есть какие-то верхнеуровневые оптимизации, которые смотрят на код как на черный ящик и стандарт запрещает их делать в случае атомиков, но это рассуждения из серии "срут ли единороги радугой" и только запутывают, см. выше почему. Ну и да, если бы такие оптимизации были, то QAtomic бы не работал :) Ведь компилятор не в курсе, что QAtomic это атомик. Thread Sanitizer, кстати, имел такую проблему - не понимал QMutex и QAtomic как механизмы синхронизации.


Название: Re: memory_order
Отправлено: Old от Май 24, 2019, 15:23
Ведь компилятор не в курсе, что QAtomic это атомик.
В курсе. Например для GCC под капотом QAtomic это https://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Atomic-Builtins.html


Название: Re: memory_order
Отправлено: Авварон от Май 24, 2019, 15:26
В курсе. Например для GCC под капотом QAtomic это https://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Atomic-Builtins.html

Ну gcc в курсе, а не gcc не в курсе=) Если вы посмотрите, то моя ссылочка и atomic-gcc в разных файлах на минуточку ;)
Сейчас-то в Qt вообще все на с++11 атомиках уже.


Название: Re: memory_order
Отправлено: Авварон от Май 24, 2019, 15:41
Т.к. никаких указаний/блокировок нет, то остается полагать что порядок выполнения AB / CD - любой. Но видимость в др. нитке срабатывает, то есть если Thread 1(B) прорвется первой и установит x - он будет видим в Thread 2. Оказывается если пишем relaxed - то записанное значение будет верно relaxed-прочитано в др нитке (конечно если оно не перекрыто опять). Однако про "всю память до того" ничего не говорится.

Да, всё верно.
Но атомики тут по большому счету ни при чем, есть понятие "когерентности кешей" - если вы что-то успешно записали в кэш по адресу 0xDEADBEEF, то другие ядра, кто будет читать этот адрес, увидят записанное значение. Если точнее, вся кэш-линия магическим образом синхронизируется.
Проблемы начинаются когда вы пишите одну переменную в одной кэш линии, а читаете из логически зависимой, но другой переменной в другой кэш-линии - они выпадают из синка т.к. процессор не в курсе, что они связаны.
Присыпьте это случайными перестановками инструкций и вы получите странные результаты.


Название: Re: memory_order
Отправлено: Igors от Май 26, 2019, 04:28
Страсти поутихли, попробуем продолжить обсуждение. Немного в сторону, но все же - а как все эти дела соотносятся с обычными действиями без std::atomic? Напр
Код
C++ (Qt)
int a = 0;
...
a = 1;
Я понимаю так: присваивание a=1 неделимо и, вероятно, выльется в одну команду. Но никаких синхронизаций кешей не выполняется. Пусть больше никто не трогает "а". Тогда записавшая нитка прочтет свою 1 (свой кеш) а остальные - возможно и 0 (если сидят на др ядре), хотя запись уже свершилась. Рано или поздно ОС синхронизирует кеши (причем достаточно скоро), но шансы получить старое значение имеются. Которое, однако, не случайный "мусор" - в данном случае оно может быть только 0 или 1.

Однако все это - всего лишь мои интуиция/предположения :) Никогда не видел "официального" объяснения (а хотелось бы). Ну здесь (https://en.cppreference.com/w/cpp/language/memory_model) такое
Цитировать
Memory order

When a thread reads a value from a memory location, it may see the initial value, the value written in the same thread, or the value written in another thread. See std::memory_order for details on the order in which writes made from threads become visible to other threads.
Как-то "не густо" :)

Еще о том же. Вот есть простецкий код
Код
C++ (Qt)
int abortFlag = 0;
 
// thread 1
 
while (!abortFlag) {
 ....
}
 
// thread 2 (main)
 
abortFlag = 1;
// thread1.joint();
Будет ли точнее (грамотнее и.т.п) юзать std::atomic<int> ? Если да то чем?

Возвращаясь к memory_order
Цитировать
Typical use for relaxed memory ordering is incrementing counters, such as the reference counters of std::shared_ptr, since this only requires atomicity, but not ordering or synchronization (note that decrementing the shared_ptr counters requires acquire-release synchronization with the destructor)
По-прежнему "не врубаюсь" в эту фразу. Атомарный инкремент должен выполняться через CAS - и никак иначе, причем тут relaxed order? Также в упор не вижу никакой половой разницы между инкрементом и декрементом. Ну может имеется ввиду что инкремент не возвращает (инкрементированное) значение, а декремент должен (но это опять догадки)


Название: Re: memory_order
Отправлено: Авварон от Май 26, 2019, 15:08
Я понимаю так: присваивание a=1 неделимо и, вероятно, выльется в одну команду.
Неверно. На каком-нибудь 16битном процессоре это выльется в две команды. Другое дело, что я не знаю, есть ли живые архитектуры где запись Инта делится. Но я и под микроконтролеры не писал да и с АРМом очень мало общался. Стандарт считает, что может - значит может.
Если вы посмотрите на какой-нибудь код типа
Код:
int m_i = 0; // в классе
m_i++; // в функции
То это может вылиться аж в ТРИ инструкции - прочитать из памяти в регистр, инкрементнуть, записать взад. То есть то, что присвоение - одна инструкция - это скорее исключение из правила.

Но никаких синхронизаций кешей не выполняется. Пусть больше никто не трогает "а". Тогда записавшая нитка прочтет свою 1 (свой кеш) а остальные - возможно и 0 (если сидят на др ядре), хотя запись уже свершилась. Рано или поздно ОС синхронизирует кеши (причем достаточно скоро), но шансы получить старое значение имеются.
Кеши синхронизируются всегда. Объяснение многопоточных проблем что, мол, в Кешах значения разные - это распространенный миф; просто так проще объяснять. Однако, кроме кешей есть регистры (они не синхронизируются) и буфера записи в кеш (там чуть хитрее но тоже никто не гарантировал синк)

Которое, однако, не случайный "мусор" - в данном случае оно может быть только 0 или 1.
В предположении, что запись Инта неделима, что, опять же, лишь предположение об архитектуре на которой будет выполняться код. На x86 всё и без атомиков всё работает ;)
В целом да, на это можно полагаться (наверное), но непонятно, зачем. Что за желание "обмануть систему"? На чем вы экономите? На спичках? Ну так на десктопе 3 из 4х атомиков раскрываются в обычные операции, то есть ассемблерный код будет одинаковый что с атомиком, что без.
Но см ниже.

Еще о том же. Вот есть простецкий код
Код
C++ (Qt)
int abortFlag = 0;
 
// thread 1
 
while (!abortFlag) {
 ....
}
 
// thread 2 (main)
 
abortFlag = 1;
// thread1.joint();
Будет ли точнее (грамотнее и.т.п) юзать std::atomic<int> ? Если да то чем?

А кто сказал, что мы обязаны читать этот int в цикле из памяти/кеша? abortFlag в данном треде не меняется, давайте положим его в регистр, сделаем if перед циклом и уйдем в бесконечный луп, если flag == 0. Я почти уверен, что такая "оптимизация" возможна начиная с с++11. Другое дело, что она вряд ли включена, потому что очень много кода сломается. Но могут включить в дальнейшем, ваш код не валиден с тз стандарта.

Возвращаясь к memory_order
Цитировать
Typical use for relaxed memory ordering is incrementing counters, such as the reference counters of std::shared_ptr, since this only requires atomicity, but not ordering or synchronization (note that decrementing the shared_ptr counters requires acquire-release synchronization with the destructor)
По-прежнему "не врубаюсь" в эту фразу. Атомарный инкремент должен выполняться через CAS - и никак иначе, причем тут relaxed order? Также в упор не вижу никакой половой разницы между инкрементом и декрементом. Ну может имеется ввиду что инкремент не возвращает (инкрементированное) значение, а декремент должен (но это опять догадки)
Схрена ли inrement - это CAS? инкремент - это инкремент, CAS -  это CAS.
То, что все, что угодно можно реализовать через CAS не значит, что нужно. Если есть атомарная "операция" инкремента - будет использована она.
Relaxed нужен, если от значения атомика не зависит поток выполнения - например, мы тупо его печатаем в консоль. И не важно, что там - 0, 42 или 100500, на результат программы это не влияет. Без relaxed атомика у вас нет гарантии, что вы вообще когда-нибудь увидите что-то, кроме начального значения. Или конечного. Или промежуточного.
Ну например:
Код:
int counter = 0; // общий счетчик
for (int i = 0; i < 10000; ++i) {doWork(); ++counter;}
может вылиться в
Код:
int counter = 0; // общий счетчик
for (int i = 0; i < 10000; ++i) {doWork();}
counter = 9999;
С тз компилятора оба куска кода делают одно и то же.  Зачем "подергивать" медленную память, если можно записать один раз в конце цикла?
Помните другой миф про volatile и многопоточность? Вот volatile как раз такие приколы лечит.


Название: Re: memory_order
Отправлено: Igors от Май 27, 2019, 08:36
Я понимаю так: присваивание a=1 неделимо и, вероятно, выльется в одну команду.
Неверно. На каком-нибудь ...
...
То это может вылиться аж в ТРИ инструкции ...
Если присваивание не "atomistic" (т.е. выполняется 2 и более командами) - то и разговаривать не о чем, никакой atomic не спасает от вклинивания между командами. Это просто не будет работать - и все.

Кеши синхронизируются всегда.
Очень в этом сомневаюсь. Тогда чем объяснить заметное падение скорости при замене обычных int'ов на atomic'и? Это мне хорошо известно по собственному опыту. Скорее всего это удовольствие достаточно дорогое.

А кто сказал, что мы обязаны читать этот int в цикле из памяти/кеша? abortFlag в данном треде не меняется, давайте положим его в регистр, сделаем if перед циклом и уйдем в бесконечный луп, если flag == 0. Я почти уверен, что такая "оптимизация" возможна начиная с с++11. Другое дело, что она вряд ли включена, потому что очень много кода сломается. Но могут включить в дальнейшем, ваш код не валиден с тз стандарта.
Здесь Вы явно перегибаете палку. В данном случае компилятору ничего не известно о неизменности abortFlag (хотя бы потому что это глобальная переменная и может быть изменена в др единице трансляции), и код будет его честно читать без всяких atomic'ов и volatile. То же касается др примера что Вы давеча приводили
Код:
data = std::make_shared<xxx> (...);
ready = true;
Нет никакой гарантии что предшествующий call никоим образом не завязан на readу, поэтому никаких перестановок не случится. Др дело так
Код:
int ready;  // локальная
...
data = std::make_shared<xxx> (...);
ready = true;
А так уже запросто переставит (особенно icc любит так делать), т.к. "гарантия неиспользования" ready есть
 
Схрена ли inrement - это CAS? инкремент - это инкремент, CAS -  это CAS.
То, что все, что угодно можно реализовать через CAS не значит, что нужно. Если есть атомарная "операция" инкремента - будет использована она.
Relaxed нужен, если от значения атомика не зависит поток выполнения - например, мы тупо его печатаем в консоль. И не важно, что там - 0, 42 или 100500, на результат программы это не влияет. Без relaxed атомика у вас нет гарантии, что вы вообще когда-нибудь увидите что-то, кроме начального значения. Или конечного. Или промежуточного.
Последняя фраза как-то не очень согласуется с предыдущим заявлением что, мол, кеши синхронизируются всегда :) Если бы так, то все нитки видели бы одно и то же, а это явно не так.
Ну хорошо, вот есть "инкремент одной командой"
Код:
inc dword_ptr[some_offset]
Да, мы никак не сможем вернуть корректное инкрементированное значение (команда-то уже кончилась). Ну ладно, допустим "оно нас не интересует". Но разве это обеспечит что именно последнее (с учетом изменений сделанных всеми нитками) значение будет инкрементировано? По-моему нет


Название: Re: memory_order
Отправлено: Авварон от Май 27, 2019, 10:10
Если присваивание не "atomistic" (т.е. выполняется 2 и более командами) - то и разговаривать не о чем, никакой atomic не спасает от вклинивания между командами. Это просто не будет работать - и все.
Если что, любой POD можно объявить как atomic. Другое дело, что он будет релизован не эффективно, см. is_lock_free() (https://en.cppreference.com/w/cpp/atomic/atomic_is_lock_free). Другое дело, что на 16битной платформе int будет 16битным и таки будет писаться одной инструкцией=)

Очень в этом сомневаюсь. Тогда чем объяснить заметное падение скорости при замене обычных int'ов на atomic'и? Это мне хорошо известно по собственному опыту. Скорее всего это удовольствие достаточно дорогое.
Тем, что вы форсите процессор/компилятор всегда писать в "память" (кэш), не используя регистры. Кроме того, помимо собственно синхронизации кешей, барьер памяти делает другие операции, а именно ждет, когда все чтения/записи станут видны, а не только самого атомика.

Здесь Вы явно перегибаете палку. В данном случае компилятору ничего не известно о неизменности abortFlag (хотя бы потому что это глобальная переменная и может быть изменена в др единице трансляции), и код будет его честно читать без всяких atomic'ов и volatile.
Компилятору известно, что в данном треде abortFlag не меняется, а также то, что он не помечен как атомик, а значит не может меняться из других тредов. Иначе он был бы помечен как атомик, верно?  ;)

То же касается др примера что Вы давеча приводили
Код:
data = std::make_shared<xxx> (...);
ready = true;
Нет никакой гарантии что предшествующий call никоим образом не завязан на readу, поэтому никаких перестановок не случится. Др дело так
Код:
int ready;  // локальная
...
data = std::make_shared<xxx> (...);
ready = true;
А так уже запросто переставит (особенно icc любит так делать), т.к. "гарантия неиспользования" ready есть
Мдааааа....
 
Последняя фраза как-то не очень согласуется с предыдущим заявлением что, мол, кеши синхронизируются всегда :) Если бы так, то все нитки видели бы одно и то же, а это явно не так.
Вы успешно проигнорировали объяснение, что кроме кешей есть регистры и буфера записи, которые не видны другим ядрам.

Ладно, давайте по-другому. Сперва вы осилите букварь (https://habr.com/ru/post/195770/) (первые 4 статьи), а потом уже поговорим.


Название: Re: memory_order
Отправлено: Igors от Май 28, 2019, 14:15
Ладно, пожуем перестановки, если к ним такой уж интерес
Компилятору известно, что в данном треде abortFlag не меняется, а также то, что он не помечен как атомик, а значит не может меняться из других тредов. Иначе он был бы помечен как атомик, верно?  ;)
"Неверно" - не то слово, это просто "бред" (как говорил тут один гуру, кстати где он - что-то как ветром сдуло).
Код:
a = 1;
b = 2;
Да, такие рудиментарные независимые операции уже десятки лет переставляются как компилятором так и процессором. Но это вовсе не распространяется на все и вся. Пример с abortFlag может быть сколь угодно примитивным, убогим и.т.п. НО он не нарушает никаких правил языка/стандарта. Разве там сказано что "нельзя писать глобальную переменную из др нитки?". Что "изменяемые переменные обязательно должны быть atomic?". Нет. А значит любой компилятор обязан создать рабочий/совместимый код, невзирая ни на какие соображения оптимизации. Иначе судьба компилятора незавидна (как и создавшей его компании).

Нетрудно привести пример когда перестановка фатальна, ну хотя бы так
Код:
mutex.lock();
a = 1;
Следуя Вашей логике и здесь "может переставить" - вот мы уже и пришли к полному абсурду :) Интересно, как программист мог бы защититься от такой перестановки и добиться лока "до"? Мне ничего не приходит в голову, кроме "никак", такая оптимизация - себе дороже. Компилятор видел передачу упр-я в "чужой" код, при этом нет гарантий что этот код не может изменить "a" или наоборот. Значит ничего переставлять он не должен. Процессор тоже не должен выполнять a=1 параллельно, увидев команду перехода. 

А в контексте atomic разговор о перестановках заходит только потому что у компилятора нет никаких оснований считать операции "зависимыми", вот и приходится создать/регламентировать явные правила для таких ситуевин.

Ладно, давайте по-другому. Сперва вы осилите букварь (https://habr.com/ru/post/195770/) (первые 4 статьи), а потом уже поговорим.
Спасибо, но таких опусов я не читаю, и Вам не советую, вреда от них больше чем пользы.


Название: Re: memory_order
Отправлено: Авварон от Май 28, 2019, 16:37
"Неверно" - не то слово, это просто "бред" (как говорил тут один гуру, кстати где он - что-то как ветром сдуло).
Ну про "бред" это вы разработчикам стандарта можете рассказать при встрече.

Код:
a = 1;
b = 2;
Да, такие рудиментарные независимые операции уже десятки лет переставляются как компилятором так и процессором.
Десятки лет стандарт ничего не говорил о том, что такое "thread" и что такое "race condition". Соответственно, разработчикам компиляторов приходилось "крутиться", предполагая худшее (то, что вы говорите что "нет гарантий что не пишется"). Есть хороший пример (из другой области) с флагом -fno-strict-aliasing, который собственно переключает компилятор между режимами "предполагать худшее" и "предполагать лучшее". Если чо, тупо включение этого флага может замедлять код на 30%.

Но это вовсе не распространяется на все и вся. Пример с abortFlag может быть сколь угодно примитивным, убогим и.т.п. НО он не нарушает никаких правил языка/стандарта.
Нарушает.
Разве там сказано что "нельзя писать глобальную переменную из др нитки?". Что "изменяемые переменные обязательно должны быть atomic?".
Сказано, еще раз:
Цитировать
When an evaluation of an expression writes to a memory location and another evaluation reads or modifies the same memory location, the expressions are said to conflict. A program that has two conflicting evaluations has a data race unless

both evaluations execute on the same thread or in the same signal handler, or
both conflicting evaluations are atomic operations (see std::atomic), or
one of the conflicting evaluations happens-before another (see std::memory_order)
If a data race occurs, the behavior of the program is undefined.


Нет. А значит любой компилятор обязан создать рабочий/совместимый код, невзирая ни на какие соображения оптимизации. Иначе судьба компилятора незавидна (как и создавшей его компании).

Начиная с С++11 - не обязан. Просто пока мы сейчас находимся в "режиме совместимости", когда стандарт уже есть, и компиляторы ему уже не противоречат, но и не делают всё, что стандарт им позволяет (ну не успели ещё написать, не всё сразу).

Нетрудно привести пример когда перестановка фатальна, ну хотя бы так
Код:
mutex.lock();
a = 1;
Следуя Вашей логике и здесь "может переставить" - вот мы уже и пришли к полному абсурду :)

Не может, lock() включает в себя барьер памяти, на той страничке, что я уже устал цитировать, есть и такие строки:
Цитировать
in particular, release of a std::mutex is synchronized-with, and therefore, happens-before acquisition of the same mutex by another thread, which makes it possible to use mutex locks to guard against data races

Компилятор видел передачу упр-я в "чужой" код, при этом нет гарантий что этот код не может изменить "a" или наоборот. Значит ничего переставлять он не должен. Процессор тоже не должен выполнять a=1 параллельно, увидев команду перехода. 

Угу, только вызов make_shared это не "чужой" код и даже не вызов функции, там всё должно инлайнится, что сводит нас к тривиальному случаю a = 1, b = 2. Вы серьезно пишете свои программы в предположении что компилятор заинлайнит или не заинлайнит что-то?=)

А в контексте atomic разговор о перестановках заходит только потому что у компилятора нет никаких оснований считать операции "зависимыми", вот и приходится создать/регламентировать явные правила для таких ситуевин.

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

Спасибо, но таких опусов я не читаю, и Вам не советую, вреда от них больше чем пользы.

Не читал, но осуждаю  ;)


Название: Re: memory_order
Отправлено: Igors от Май 29, 2019, 11:41
Сказано, еще раз:
Цитировать
When an evaluation of an expression writes to a memory location and another evaluation reads or modifies the same memory location, the expressions are said to conflict. A program that has two conflicting evaluations has a data race unless

both evaluations execute on the same thread or in the same signal handler, or
both conflicting evaluations are atomic operations (see std::atomic), or
one of the conflicting evaluations happens-before another (see std::memory_order)
If a data race occurs, the behavior of the program is undefined.

Вы исходите из установки типа "UB = ппц, допускать его никак нельзя!" Это перегиб. Да, без атомиков мы можем увидеть abortFlag==1, но можем и abortFlag==0 (хотя 1 уже записана), в этом собсно и заключается "undefined". И что? Чем это нам грозит? Ну крутанется еще раз тело while, поймаем единичку на след итерации, это нормально, нитку мгновенно не остановить.

Не может, lock() включает в себя барьер памяти,
...
Угу, только вызов make_shared это не "чужой" код
Ну Вы же прекрасно понимаете что "частные случаи" ничего не доказывают. Тот же mutex может быть моим классом, а тот же make_shared - звать мой конструктор.

Да, и именно поэтому надо помечать явно атомиками то, что дергается из разных тредов.
Опять как-то у Вас все "очень просто", типа "натыкать атомиков побольше - и будет счастье" :) Скорее эффект будет обратный - скорость подсядет на lock, код засорится, но без всяких достижений. Впрочем это обычный рез-т чрезмерно усердного чтения.

Не читал, но осуждаю  ;)
Да :) Глянув первые пару фраз я быстро понял что речь пойдет обо всем-всем. Очень может быть что там есть вещи что я не постиг при написании своих lock-free великов, но их будет не так уж много. И как-то фильтровать/вылавливать эти "жемчужные зерна" - нет уж, увольте. А "беглый просмотр по диагонали" практически бесполезен.


Название: Re: memory_order
Отправлено: Авварон от Май 29, 2019, 12:36
Вы исходите из установки типа "UB = ппц, допускать его никак нельзя!" Это перегиб. Да, без атомиков мы можем увидеть abortFlag==1, но можем и abortFlag==0 (хотя 1 уже записана), в этом собсно и заключается "undefined". И что? Чем это нам грозит? Ну крутанется еще раз тело while, поймаем единичку на след итерации, это нормально, нитку мгновенно не остановить.
UB допускать нельзя потому что это не "нолик" или "единичка", это ваш внезапно умерший кот или клоун, прыгающий у вас на носу. UB значит, что может случиться всё, что угодно. Я даже вам привел пример, какую оптимизацию может делать компилятор начиная с с++11 - а именно предполагать, что переменная НЕ шарится между тредами. Иначе можно быстро дорассуждаться до того, что компилить каждый инт как atomic.

Ну Вы же прекрасно понимаете что "частные случаи" ничего не доказывают. Тот же mutex может быть моим классом, а тот же make_shared - звать мой конструктор.
Давайте поговорим о конструкторе и "чужом" коде. Вспоминаем статью (https://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf) Александреску и Мейерса про сиглтоны.
В частности, такой код:
Код:
if (!m_instance) {
    std::unique_lock<std::mutex> l(m_mutex);
    if (!m_instance)
        m_instance = new Singleton();
}
return m_instance;
Код безопасный? Ну а чо, присваивание указателя "атомарно" же, как любят рассказывать "свидетели атомарного инта". Развернем немного что происходит под капотом:
Код:
if (!m_instance) {
    auto tmp = malloc(sizeof (Singleton));
    new (tmp) Singleton();
    m_instance = tmp;
}
А если компилятор не использует временную переменную (с чего бы, кстати?):
Код:
if (!m_instance) {
    m_instance = malloc(sizeof (Singleton));
    // <=== ой, тут пробой - указатель УЖЕ не ноль, а объекта ещё нет
    new (m_instance) Singleton();
}
А даже если использует:
Код:
if (!m_instance) {
    auto tmp = malloc(sizeof (Singleton));
    m_instance = tmp; // процессор сделал reorder
    // <=== ой, тут пробой
    new (tmp) Singleton(); // предположим что тут "вызов" заинлайнился и в конструкторе мы не юзаем m_instance, т.е. нет зависимости по данным
}
Помог мьютекс? Помогло "атомарное" присваивание указателя?
Если что, статья написана во времена до с++11 и все эти проблемы хорошо известны и решение то же, что и с с++11 - юзайте барьеры памяти:
Цитировать
The general solution is to use memory barriers (i.e., fences)


Опять как-то у Вас все "очень просто", типа "натыкать атомиков побольше - и будет счастье" :) Скорее эффект будет обратный - скорость подсядет на lock, код засорится, но без всяких достижений. Впрочем это обычный рез-т чрезмерно усердного чтения.
У меня тут также говорят, а потом я две недели исправляю баг связанный с тем, что один объект дергается из двух тредов безо всяких синхронизаций. Ну а чо, мьютексы это ж медленно. Парсить гигабайтные файлы через QString::split быстро, а мьютексы медленно. Две недели это занимает потому, что попытка полечить одно ломает код в другом рандомном месте (например, внутри QLocale, ибо там тоже "забыли" что QLocale::system() может ВНЕЗАПНО дергаться из разных тредов), и нельзя просто взять и перенести код в "правильный" тред, тайминги поехали, хрупкое равновесие нарушилось и всё щячло попячься ололо пыщ-пыщ.

Глянув первые пару фраз я быстро понял что речь пойдет обо всем-всем. Очень может быть что там есть вещи что я не постиг при написании своих lock-free великов, но их будет не так уж много. И как-то фильтровать/вылавливать эти "жемчужные зерна" - нет уж, увольте. А "беглый просмотр по диагонали" практически бесполезен.
Речь идет обо всем-всем потому что необходимо понимание "всего-всего" чтобы не было мифов про всемогущий volatile, про "разъехавшиеся кеши" и про "атомарный int".


Название: Re: memory_order
Отправлено: Igors от Май 29, 2019, 15:12
// <=== ой, тут пробой - указатель УЖЕ не ноль, а объекта ещё нет
Да, это подлянка известная. В одном из компиляторов что мне встречались даже была опция чтобы это пресекать

// процессор сделал reorder
Давно хотел заметить что это высказывание небезупречно (хотя по существу верно). Думаю ничего "переставлять" процессор не умеет. Но он может одновременно исполнять "пачку" команд, при этом результат исполнения последующих может сформироваться раньше чем предыдущих, т.е. эффект тот же.

Если что, статья написана во времена до с++11 и все эти проблемы хорошо известны и решение то же, что и с с++11 - юзайте барьеры памяти:
Цитировать
The general solution is to use memory barriers (i.e., fences)
Вы прямо-таки навлекаете на себя (справедливый) гнев читателя букваря! :)
Цитировать
Кароч, как правельно? Не сказал, вычитал в инете и измывается, гад
Ну ладно, мы не ищем легких путей. Откроем статью
Код
C++ (Qt)
Singleton* Singleton::instance ()
{
  Singleton* tmp = pInstance;
 
...// insert memory barrier
 
  if(tmp == 0) {
     Lock lock;
     tmp = pInstance;
     if (tmp == 0) {
        tmp = new Singleton;
 
...// insert memory barrier
 
        pInstance = tmp;
    }
  }
  return tmp;
}
Ну и в качестве барьера видимо надо заюзать
Код
C++ (Qt)
std::atomic_thread_fence(std::memory_order_acquire);
Согласен, хорошее, грамотное решение. Правда не вижу зачем первый-то барьер понадобился?
И все-таки рискну предложить свою версию (без барьеров)
Код
C++ (Qt)
if (!m_instance) {
   std::unique_lock<std::mutex> l(m_mutex);
   if (!m_instance) {
       auto temp = new Singleton();
       if (temp)
         m_instance = temp;
   }
}
return m_instance;
 
UB допускать нельзя потому что это не "нолик" или "единичка", это ваш внезапно умерший кот или клоун, прыгающий у вас на носу. UB значит, что может случиться всё, что угодно. Я даже вам привел пример..
Я тоже Вам привел цитату что при чтении памяти могут быть варианты, но отнюдь не "что угодно"

Речь идет обо всем-всем потому что необходимо понимание "всего-всего" чтобы не было мифов про всемогущий volatile, про "разъехавшиеся кеши" и про "атомарный int".
Ну каждый из этих мифов имеет весомые основания и не раз подтверждался на практике. Поэтому все они и подвержены (массовым) злоупотреблениям, т.е. тыкаются везде и без разбора. Замечу что Ваше использование атомиков тоже очень смахивает на очередной миф  :)


Название: Re: memory_order
Отправлено: Авварон от Май 29, 2019, 17:24
Думаю ничего "переставлять" процессор не умеет. Но он может одновременно исполнять "пачку" команд, при этом результат исполнения последующих может сформироваться раньше чем предыдущих, т.е. эффект тот же.
И в чем разница?)

Кароч, как правельно? Не сказал, вычитал в инете и измывается, гад
Правильно использовать синглтон Майерса. Начиная с С++11 стандарт гарантирует что переменная будет инициализирована потокобезопасно и ровно один раз:
Код:
Singleton *instance() {
    static Singleton m_instance;
    return &m_instance;
}

Ну ладно, мы не ищем легких путей. Откроем статью
...
Ну и в качестве барьера видимо надо заюзать
Код
C++ (Qt)
std::atomic_thread_fence(std::memory_order_acquire);
Согласен, хорошее, грамотное решение. Правда не вижу зачем первый-то барьер понадобился?
Можно (и нужно) делать сам указатель атомиком, 8й слайд (https://www.kdab.com/wp-content/uploads/stories/slides/Day2/OliverGoffart_lockfree_0.pdf)
Первый барьер нужен ровно для того, для чего mutex::lock() нужно звать и на стороне читателя, и на стороне писателя. "Половинчатый" лок (и барьер) не защищает ни от чего.

И все-таки рискну предложить свою версию (без барьеров)
Код
C++ (Qt)
if (!m_instance) {
   std::unique_lock<std::mutex> l(m_mutex);
   if (!m_instance) {
       auto temp = new Singleton();
       if (temp)
           m_instance = temp;
   }
}
return m_instance;
 
Круто. new не может вернуть 0, а значит компилятор может выкинуть вашу проверку, а значит, это ничем не отличается от кода выше=)

Я тоже Вам привел цитату что при чтении памяти могут быть варианты, но отнюдь не "что угодно"
Ну так и с указателем выше может быть только 0 или не 0, чо никак спасает от краша. Кроме невалидного значения там полно иных проблем, вызывающих UB. Что-то переставилось, что-то осело в регистре, где-то вместо временной переменой компилятор записал напрямую, где-то наоборот, добавил переменную, и всё, понеслась - предсказать результат программы нельзя.
Можно конечно жульничать и рассказывать всем что "у меня же работает" а можно взять и написать программу без UB и не тратить две недели на отладку.
Попытки быть "очень умным" разбиваются о то, что 99% программистов не понимает (включая меня), как работают современные ЦПУ, все знания на "школьном" уровне ассемблера из универа. Вы делаете какие-то предположения об архитектуре не имея не малейшего понятия, как оно там реально устроено. В чем смысл гадания на кофейной гуще? Есть стандарт, который четко говорит что есть UB, а что нет. Пока ваши попытки "сжульничать" с тем же синглтоном выглядят весьма неубедительно.

Я тоже Вам привел цитату что при чтении памяти могут быть варианты, но Ну каждый из этих мифов имеет весомые основания и не раз подтверждался на практике. Поэтому все они и подвержены (массовым) злоупотреблениям, т.е. тыкаются везде и без разбора. Замечу что Ваше использование атомиков тоже очень смахивает на очередной миф  :)
Солнце встает на востоке и садится на западе, значит, вращается вокруг земли=) А что, подтверждается же на практике.
Ну и да, я не использую атомики, я использую мьютексы, потому что написание корректного lock-free алгоритма требует те же недели времени. Не верите - загляните в QMutex.


Название: Re: memory_order
Отправлено: Igors от Май 30, 2019, 06:09
Ну так и с указателем выше может быть только 0 или не 0, чо никак спасает от краша. Кроме невалидного значения...
С "невалидным значением" Вы просто неправы. Обратите внимание на начало примера классиков
Код
C++ (Qt)
Singleton* Singleton::instance ()
{
  Singleton* tmp = pInstance;
...
 
Совершенно бесхитростное чтение указателя из памяти, причем в данный момент этот указатель может писаться др ниткой. Если считанный tmp ненулевой - он принимается и считается верным. Если принять Вашу версию о мусоре - эта конструкция рухнула бы немедленно. Я понимаю что
Цитировать
тестирование может показать наличие багов - но не их отсутствие
И сам имел баги всплывшие через годы, но все-таки вcе хорошо в меру, стремление к абсолютной/идеальной корректности заметно вредит. Заметим что атомарное чтение здесь добавит тормозов но ситуацию никак не изменит - ну да, "не ноль" поймаем быстрее, но если ноль - все то же

...там полно иных проблем, вызывающих UB. Что-то переставилось, что-то осело в регистре, где-то вместо временной переменой компилятор записал напрямую, где-то наоборот, добавил переменную, и всё, понеслась - предсказать результат программы нельзя.
О чем конкретно Вы говорите? О данной реализации синглтона? Не вижу где чего здесь "сдвинется". Как говорят буржуины "please be more concrete"

Можно конечно жульничать и рассказывать всем что "у меня же работает" а можно взять и написать программу без UB и не тратить две недели на отладку.
Попытки быть "очень умным" разбиваются о то, что 99% программистов не понимает (включая меня), как работают современные ЦПУ, все знания на "школьном" уровне ассемблера из универа. Вы делаете какие-то предположения об архитектуре не имея не малейшего понятия, как оно там реально устроено. В чем смысл гадания на кофейной гуще? Есть стандарт, который четко говорит что есть UB, а что нет. Пока ваши попытки "сжульничать" с тем же синглтоном выглядят весьма неубедительно.
Такой хрд мысли очень хорош когда "абсолютно корректное" решение дешево - достаточно лишь покопаться в бааальшой каропке (кросс-платформенной) :) Но мне почему-то часто не удается отсидеться у кормушки (наверное не везет).

Ну и да, я не использую атомики, я использую мьютексы, потому что написание корректного lock-free алгоритма требует те же недели времени. Не верите - загляните в QMutex.
Да, для написания каких-то общих вещей lock-free весьма затратен. Но есть масса мелких задачек (ни или даже под-задачек) где он вписывается прекрасно. Ну хотя бы

- N ниток заполняют один контейнер (сливают туда рез-ты). Или берут из контейнера задания. Или и то и другое. Для простоты размер контейнера фиксирован. Бум засисяться мутексами или как?


Название: Re: memory_order
Отправлено: Авварон от Май 30, 2019, 12:34
С "невалидным значением" Вы просто неправы. Обратите внимание на начало примера классиков
Код
C++ (Qt)
Singleton* Singleton::instance ()
{
  Singleton* tmp = pInstance;
...
 
Совершенно бесхитростное чтение указателя из памяти, причем в данный момент этот указатель может писаться др ниткой. Если считанный tmp ненулевой - он принимается и считается верным. Если принять Вашу версию о мусоре - эта конструкция рухнула бы немедленно.
Ну так у классиков куча примеров как работать НЕ будет и ни одного как будет (кроме того гипотетического "тут нужен барьер"). На тот момент не было атомиков и "барьер" можно было организовать только директивой процессору/компилятору (тот код что был пару страниц назад). Так как это непереносимо, они не могли сделать адекватный пример.

О чем конкретно Вы говорите? О данной реализации синглтона? Не вижу где чего здесь "сдвинется". Как говорят буржуины "please be more concrete"

Код:
Singleton::Singleton() : m_ptr(new int) {} // вот наш конструктор

if (!m_instance) {
    std::unique_lock<std::mutex> l(m_mutex);
    if (!m_instance) {
        auto temp = new Singleton();
        if (temp)
            m_instance = temp;
    }
}
return m_instance;
Итого, мы делаем
0. lock()
1. m_ptr = new int
2. if (temp) m_instance = temp
3. unlock()
4. if (m_instance)
5. *m_instance->m_ptr;

Зависимости по данным нет, а значит процессор может "переставить":
0. lock()
1. if (temp) m_instance = temp
2. m_ptr = new int
3. unlock()
4. if (m_instance)
5. *m_instance->m_ptr;

Теперь вспоминаем как на самом деле работают барьеры (данные станут видны при следующем ВХОДЕ в барьер, а не ДО выхода). А так как следующего барьера (на чтение) нет, то получаем веселое:
0. lock()
1. unlock()
2. if (temp) m_instance = temp
// барьер нужен ТУТ
3. if (m_instance)
4. *m_instance->m_ptr; // crash
5. m_ptr = new int

Но на x86 у вас этого не будет, потому что STORE-STORE переставлять он не умеет. Поэтому вы не даже не сможете отладить ваш синглтон.

Да, для написания каких-то общих вещей lock-free весьма затратен. Но есть масса мелких задачек (ни или даже под-задачек) где он вписывается прекрасно. Ну хотя бы

- N ниток заполняют один контейнер (сливают туда рез-ты). Или берут из контейнера задания. Или и то и другое. Для простоты размер контейнера фиксирован. Бум засисяться мутексами или как?
Мы уже вроде выяснили, что запись указателя (на примере синглтона) "не атомарна".
Значит, если кто-то пишет, а кто-то потребляет ячейку массива, то нужно предохраняться.

Есть два юзкейза, когда не надо мьютексов.
1. Данные константны и были созданы ДО старта треда (старт треда это барьер). Пример - QApplication.
2. Есть разделение по данным.

Умные люди используют 2. Если тред 1 пишет в ячейку массива 0, а тред 2 - в ячейку массива 1, то атомики и мьютексы не нужны. В ином случае ЛЮБОЕ разделяемое владение должно быть защищено. Даже если это флажок. В случае флажка можно схитрить и заюзать томик вместо мьютекса. В случае шаред_птра/QString уже и атомиком не обойтись, нужен полноценный лок.


Название: Re: memory_order
Отправлено: Igors от Июнь 01, 2019, 04:17
Зависимости по данным нет, а значит процессор может "переставить":
Правду сказать, не уловил Ваш псевдокод. Но в любом случае если принять версию о мудаковатом процессоре который как хочет так и переставляет, то проблемы конечно будут. Тогда проще всего сделать m_instance атомиком и читать acquire, писать release (тут много ума не надо). НО это ведь никак не связано с первым чтением которое - чистой воды оптимизация. Ненулевое значение по-прежнему "истинно", нулевое по-прежнему "ложно" и его надо перепроверять.

Умные люди используют 2. Если тред 1 пишет в ячейку массива 0, а тред 2 - в ячейку массива 1, то атомики и мьютексы не нужны. В ином случае ЛЮБОЕ разделяемое владение должно быть защищено. Даже если это флажок. В случае флажка можно схитрить и заюзать томик вместо мьютекса. В случае шаред_птра/QString уже и атомиком не обойтись, нужен полноценный лок.
Ну вот напр надо попыксельно пересчитать хороший имедж.
Код
C++ (Qt)
void run( QImage & image, QAtomicInt & rowCount )
{
 while (true) {
  int row = rowCount++;
  if (row >= image.height()) return;
 
  uchat * pix = image.scanLine(row);
  for (int x = 0; x < image.width(); ++x)
    // парим пыксели  
 }
}
Конечно можно было и по-другому, напр дать каждой нитке " с какой строки по какую", но это объективно слабее (нет перераспределения нагрузки). Вот Вам и "lock-free алгоритм".


Название: Re: memory_order
Отправлено: Авварон от Январь 01, 2000, 05:38
Ну так любой map() прекрасно параллелится и не требует синхронизаций:
Код:
y[i] = foo(x[i])

Это известный факт и объясняется тем, что есть разделение по данным - ячейки массива независимы.

Вот reduce уже другое дело.


Название: Re: memory_order
Отправлено: Авварон от Июль 10, 2019, 16:44
Хе-хе, к разговору о мьютексах, атомиках и UB. Обновил gcc с 4.8 до 8.2 код ВНЕЗАПНО стал падать где раньше не падал (при обращении к мемберу из разных тредов).
А вы "ну у меня же работает" :)


Название: Re: memory_order
Отправлено: ViTech от Июль 10, 2019, 22:35
Обновил gcc с 4.8 до 8.2 код ВНЕЗАПНО стал падать где раньше не падал (при обращении к мемберу из разных тредов).

Пример кода можно? Защита хоть какая была?


Название: Re: memory_order
Отправлено: Авварон от Июль 11, 2019, 00:39
Обновил gcc с 4.8 до 8.2 код ВНЕЗАПНО стал падать где раньше не падал (при обращении к мемберу из разных тредов).

Пример кода можно? Защита хоть какая была?

Да нет ничего, о том и речь...