Нашли или выдавили из себя код, который нельзя назвать нормальным,
на который без улыбки не взглянешь?
Не торопитесь его удалять или рефакторить, — запостите его на
говнокод.ру, посмеёмся вместе!
#include <cstdlib>
#include <chrono>
#include <iostream>
#include <thread>
int p = 0;
int *q = nullptr;
void g()
{
using namespace std::chrono_literals;
std::cout << "g()" << std::endl;
std::cout << "g(): p = 1" << std::endl;
p = 1;
std::this_thread::sleep_for(1s);
if (q != nullptr) {
std::cout << "g(): *q = 1" << std::endl;
*q = 1;
} else {
std::cout << "g(): q == nullptr" << std::endl;
}
}
void f()
{
using namespace std::chrono_literals;
std::cout << "f()" << std::endl;
if (p == 0) {
std::cout << "f(): first loop start" << std::endl;
while (p == 0) { } // Потенциально конечный
std::cout << "f(): first loop end" << std::endl;
}
int i = 0;
q = &i;
std::cout << "f(): second loop start" << std::endl;
while (i == 0) { } // Потенциально конечный, хотя в условии только автоматическая пельменная
std::cout << "f(): second loop end" << std::endl;
}
int main()
{
using namespace std::chrono_literals;
std::cout << "f() thread start" << std::endl;
auto thr1 = std::thread(f);
thr1.detach();
std::this_thread::sleep_for(1s);
std::cout << "g() thread start" << std::endl;
auto thr2 = std::thread(g);
thr2.detach();
std::this_thread::sleep_for(2s);
std::cout << "Done" << std::endl;
std::_Exit(EXIT_SUCCESS);
}
Ожидание:
f() thread start
f()
f(): first loop start
g() thread start
g()
g(): p = 1
f(): first loop end
f(): second loop start
g(): *q = 1
f(): second loop end
Done
The implementation may assume that any thread will eventually do one of the following:
— terminate,
— make a call to a library I/O function,
— access or modify a volatile object, or
— perform a synchronization operation or an atomic operation.
В f() нет ни I/O, ни доступа к волатильным объектам, ни атомарных операций.
Ну более того, на каких-то платформах это может повиснуть даже без оптимизации.
Это на x86 мы привыкли к полной когерентности, а есть ведь платформы с более слабой моделью памяти, где проц тупо не будет перезагружать кешлайн, в котором лежит p или i. Ты ведь не дал ему такого повода.
Реальна ли такая схема на платформе без когерентности:
1. В кеше ядра1 переменная равна 1
2. В кеше ядра2 переменная равна 2
3. тред1 запускается то на первом ядре, то на втором, и с его точки зрения переменная равно то одному, то другому, пока барьер не поставят?
volatile гарантирует, что конпелятор не будет выёбываться, переставляя и выбрасывая записи и чтения. В общем-то и всё.
Это важно для обращения ко всяким железкам, когда порядок записей и даже чтений имеет значение.
Ну и в обработчике прерываний, если у тебя с ним какие-то переменные расшарены.
volatile предотвращает OoO (реордеринг) на одном ядре. И не даёт компилеру полностью выкидывать такие циклы.
Every access (read or write operation, member function call, etc.) made through a glvalue expression of volatile-qualified type is treated as a visible side-effect for the purposes of optimization (that is, within a single thread of execution, volatile accesses cannot be optimized out or reordered with another visible side effect that is sequenced-before or sequenced-after the volatile access. This makes volatile objects suitable for communication with a signal handler, but not with another thread of execution
Опять же, я считаю это не совсем правдой. volatile можно использовать для синхронизации если старая-добрая одноядерная машина.
>у нас в жабе всё проще
Я бы не сказал. Оно везде сложно.
Они в 8ой вроде fence из крестов навезли.
ну я примерно это и сказал: волатильность заставляет компилятор распологать код последовательно и не оптимизировать. Но процессору ничего не мешает выполнять код как ему угодно, если ему срать на треды на других ядрах.
А фенсы заставляют его остановиться, и флашнтуь буфер в кеш, из которого уже по всеми этим MESI/MESA (вечно их путаю) он растечется по кешам остальных ядер.
В джаве ЕМНИП есть понятие "happens before", и оно определяет обе ситуации: и хардварную и софтварную грубо говоря.
Если я говорю, что Foo happened before Bar, то Bar видит ВСЁ.
Потому в джаве достаточно пометить переменную волатильно, и это будет значить, что запись в нее автоматически сделает ее доступной для других ядер.
Другой вопрос, в каком месте будет тред на другом ядре мы не знаем, и тут уже нужны примитивны синхронизации (которые тоже гарантируют хеппнс бифор, так что не нужно переменные волатилить)
>Они в 8ой вроде fence из крестов навезли.
Это какой-то ансфейс для царей же, не?
> volatile можно использовать для синхронизации
> если старая добрая одноядерная машина
Нет. Разве что в примитивных случаях, где relaxed хватает. Насколько помню, volatile не является конпеляторным барьером и не запрещает переупорядочивать обычные записи вокруг себя. Только другие volatile и сайд-эффекты. Т.е. тот же мутекс из волатайла получится весьма хуёвый.
> Т.е. тот же мутекс из волатайла получится весьма хуёвый.
Да.
> Разве что в примитивных случаях, где relaxed хватает.
Да. Именно тот пример, который я привёл. Когда переменная меняется один раз за всё время работы программы.
Даже пытаться использовать его как атомик — путь в ад.
а вот атомарное чтение и запись в него вроде совсем не гарантируется.
Если я в 32х битном режиме пишу в 64х битную переменную, даже в волатильную, то ничего не помешает другому треду увидеть в ней мусор в середине исполнения, не?
Именно потому я за "атомарные инструкции в процессоре")
Кстати, в джаве точно так же вроде: там запись например в long не гарантирует атомарности, даже если он волатильный, так что есть спец классы для этого (ну или надо юзать примитивы синхронизации_)
приведи пример, когда на одноядерной машине volatile нельзя юзать как мутекс
>не является конпеляторным барьером
запись в волатилку всегда имеет сайд эффект, а значит код ПОСЛЕ него должен быть реально исполнен после этой записи, не?
То есть если другой код не волатильный, то он не может увидеть случайно сайд эффект?
volatileFoo = 12;
doall(volatileBar); //этот код выполнится после
doall(volatileFoo); //И этот код выполнится после
doall(bar); //А этот может когда угодно
int data;
volatile int mutex;
// было
data = 42;
lock = 0; // release mutex
// стало
lock = 0;
data = 42;
Второй тред увидит отпущенную лочку, пойдёт читать данные и наебнётся. Если же вместо volatile взять атомик с release семантикой, то такой хуйни не будет.
ну да, потому что data никак не зависит от лока, понятно, спасибо.
так что как мютекс его можно юзать только если ты будешь трогать другие волатильные переменные, потому что про них компилятор не знает, от чего они зависят
а в x86 писание в память упорядоченное, так что он гарантирует тебе выпёздывание буфера в кеш в том порядке, в каком его заполнили? или почему обязательно нужен арм?
Да, в арме не было единого глобального порядка записей, который наблюдают все процы. Теперь походу есть. И для эксперимента подойдёт только какой-нибудь power pc.
Самый мощный проц это Alpha, царство ему небесное
Вика пишет, что Dependent loads can be reordered, хотя мне это не очень понятно. Видимо бывают сорта депенденсов.
А что у ваших армов с кококококогерентностью кеша? попадание в кеш-то гарантировано бродкастится во все кеши, или тоже нужно явно?
Ну смотри, даже на интеле у тебя запись может застрять во write-back кеше и годами висеть в нём, не попадая в оперативку. Но за счёт протокола когерентности ядро с циклом об этой записи узнает.
Если у тебя там какая-то хуйня с тыщей ядер, то им будет очень дорого следить за кешами друг-друга. В лучшем случае они будут мониторить только реальные записи в память (т.е. тебе понадобится write-through семантика на done = 1). В худшем случае они вообще ничего не будут мониторить (и тогда нужно чтение с инвалидацией кеша на !done). volatile ничего из этого не даёт.
>что это были ядерные треды, которые никто никогда не вытеснит.
Я вижу только одну ситуацию, что они не увидят друг-друга: в какой-то NUMA машине из нескольких сокетов. Но в таких обычно стараются делать NUMA-aware пулы чтобы потоки взаимодействовали в рамках своего сокета.
>атомарное чтение уже включено в цену (интел, арм)
Разве? Там же лишний LFENCE будет на каждом чтении, не?
Ничего не понимаю... И это программисты? Говно какое-то... Пидоры, блядь. Родина им дала практические задачи! Решай, решай задачи из предметной области — блядь, не хочу, хочу жрать говно! Что такое? Это программирование?! Это программирование?! Суки... Ядер понакупили, заборов и мьютексов понаставили! Говно жрут! Пидоры, блядь, ёбаные...
Реальный пример: https://ideone.com/OCJQg9 («Wandbox» лежит, зараза).
А чтобы получить ожидаемый вывод — надо конпелировать с «-O0».
Выхлоп компилятора прекрасен (чуть упрощённый кот): https://gcc.godbolt.org/z/Kx4xeE
1.10 Multi-threaded executions and data races
The implementation may assume that any thread will eventually do one of the following:
— terminate,
— make a call to a library I/O function,
— access or modify a volatile object, or
— perform a synchronization operation or an atomic operation.
В f() нет ни I/O, ни доступа к волатильным объектам, ни атомарных операций.
Какой карманный лев )))
Это на x86 мы привыкли к полной когерентности, а есть ведь платформы с более слабой моделью памяти, где проц тупо не будет перезагружать кешлайн, в котором лежит p или i. Ты ведь не дал ему такого повода.
Реальна ли такая схема на платформе без когерентности:
1. В кеше ядра1 переменная равна 1
2. В кеше ядра2 переменная равна 2
3. тред1 запускается то на первом ядре, то на втором, и с его точки зрения переменная равно то одному, то другому, пока барьер не поставят?
Это важно для обращения ко всяким железкам, когда порядок записей и даже чтений имеет значение.
Ну и в обработчике прерываний, если у тебя с ним какие-то переменные расшарены.
Какой хардкор) у нас в жабе всё проще
Every access (read or write operation, member function call, etc.) made through a glvalue expression of volatile-qualified type is treated as a visible side-effect for the purposes of optimization (that is, within a single thread of execution, volatile accesses cannot be optimized out or reordered with another visible side effect that is sequenced-before or sequenced-after the volatile access. This makes volatile objects suitable for communication with a signal handler, but not with another thread of execution
Опять же, я считаю это не совсем правдой. volatile можно использовать для синхронизации если старая-добрая одноядерная машина.
>у нас в жабе всё проще
Я бы не сказал. Оно везде сложно.
Они в 8ой вроде fence из крестов навезли.
А фенсы заставляют его остановиться, и флашнтуь буфер в кеш, из которого уже по всеми этим MESI/MESA (вечно их путаю) он растечется по кешам остальных ядер.
В джаве ЕМНИП есть понятие "happens before", и оно определяет обе ситуации: и хардварную и софтварную грубо говоря.
Если я говорю, что Foo happened before Bar, то Bar видит ВСЁ.
Потому в джаве достаточно пометить переменную волатильно, и это будет значить, что запись в нее автоматически сделает ее доступной для других ядер.
Другой вопрос, в каком месте будет тред на другом ядре мы не знаем, и тут уже нужны примитивны синхронизации (которые тоже гарантируют хеппнс бифор, так что не нужно переменные волатилить)
>Они в 8ой вроде fence из крестов навезли.
Это какой-то ансфейс для царей же, не?
Они из java memory model его и заимствовали 🙂
Но в лучших традициях С++, они поступили согласно принципу: давайте возьмём концепт и сделаем его в 10 раз сложнее.
Просто в Йажа volatile был full fence. Крестобляди рассудили что это черезчур медленно и черезчур просто и добавили 4 разных memory_order.
да, именно)
Потому в яже мне было срать на сорта заборов. Хепнс бефор куда более внятная и простая конструкция
Но она хуёво ложится на разнообразие железа и низкоуровневых инструкций. (MFENCE, LFENCE, SFENCE)
И не всегда имеет оптимальный пирформанс, накладывая своими гарантиями лишние ограничения на компилятор.
Во многих случаях достаточно memory_order_relaxed.
Именно поэтому я за «C++».
угу)
ну это же вечный трейдофф: "много думать" versus тормоза.
Вот в питоне люди юзают GIL, и им куда проще жить
когда вообещ в кресты завезли мемори модел для тредов? C++11?
> если старая добрая одноядерная машина
Нет. Разве что в примитивных случаях, где relaxed хватает. Насколько помню, volatile не является конпеляторным барьером и не запрещает переупорядочивать обычные записи вокруг себя. Только другие volatile и сайд-эффекты. Т.е. тот же мутекс из волатайла получится весьма хуёвый.
Да.
> Разве что в примитивных случаях, где relaxed хватает.
Да. Именно тот пример, который я привёл. Когда переменная меняется один раз за всё время работы программы.
Даже пытаться использовать его как атомик — путь в ад.
Если я в 32х битном режиме пишу в 64х битную переменную, даже в волатильную, то ничего не помешает другому треду увидеть в ней мусор в середине исполнения, не?
Кстати, в джаве точно так же вроде: там запись например в long не гарантирует атомарности, даже если он волатильный, так что есть спец классы для этого (ну или надо юзать примитивы синхронизации_)
>не является конпеляторным барьером
запись в волатилку всегда имеет сайд эффект, а значит код ПОСЛЕ него должен быть реально исполнен после этой записи, не?
так?
так что как мютекс его можно юзать только если ты будешь трогать другие волатильные переменные, потому что про них компилятор не знает, от чего они зависят
Любому вменяемому человеку понятно что использование volatile в качестве примитива синхронизации — полная хуйня.
Особенно в свете выхода С++11 и появления широкого набора кроссплатформенных альтернатив.
Например, у нас есть три булевые переменные, и тред1 делает
Если они волатильные, то я могу гарантировать, что другой тред увидит step3Completed ПОСЛЕ того, как он увидит step1Completed если машина одноядерная.
А если двухядерная, то ничего не мешает процессору сбросить в кеш из буфера step2Completed, а остальные не сбросить.
Потому что volatile не ставит fence.
И другой тред увидит их в любом порядке
верно?
Спасибо что напомнил, я когда-то хотел попробовать, но у меня не было подходящего железа.
Вика пишет, что Dependent loads can be reordered, хотя мне это не очень понятно. Видимо бывают сорта депенденсов.
А что у ваших армов с кококококогерентностью кеша? попадание в кеш-то гарантировано бродкастится во все кеши, или тоже нужно явно?
В джаве вроде бы нет, потому volatile сделает fence.
Жаль конечно, что нет такой профессии: "пиздун про интересную хуйню", я бы устроился
То получится вот это:
Рекомендую почитать JCP, там все эти вопросы очень подробно разобраны.
оказалось, что это тот же неймспейс
https://en.cppreference.com/w/cpp/atomic/atomic/compare_exchange
Я уже вроде приводил контр-пример.
То, что на интеле всё когерентно и в атомик риде нет барьеров и специальных инструкций, поэтому и обычное чтение сойдёт?
>То, что на интеле всё когерентно
А на других платформах разве будут проблемы?
У нас есть признак шатдауна. Он может поменяться только одним способом (из 0 в 1).
Как только он стал ненулевым рано или поздно все это заметят и остановятся.
Если у тебя там какая-то хуйня с тыщей ядер, то им будет очень дорого следить за кешами друг-друга. В лучшем случае они будут мониторить только реальные записи в память (т.е. тебе понадобится write-through семантика на done = 1). В худшем случае они вообще ничего не будут мониторить (и тогда нужно чтение с инвалидацией кеша на !done). volatile ничего из этого не даёт.
Я к тому что атомик-чтения и особенно мьютексы были бы слишком дорогими в этой ситуации с флагом.
volatile раньше был вполне адекватным методом.
Но в целом после завоза в кресты std::memory_order и happens-before семантики оно бесполезно.
Когда юзер психанёт и ткнёт в резет, ага. Представь, что это были ядерные треды, которые никто никогда не вытеснит.
> слишком дорогими
Да вот нихуя. Либо атомарное чтение бесплатное уже включено в цену (интел, арм) либо без него твой код тупо не работает (альфа?)
Я вижу только одну ситуацию, что они не увидят друг-друга: в какой-то NUMA машине из нескольких сокетов. Но в таких обычно стараются делать NUMA-aware пулы чтобы потоки взаимодействовали в рамках своего сокета.
>атомарное чтение уже включено в цену (интел, арм)
Разве? Там же лишний LFENCE будет на каждом чтении, не?
Зачем? Зачем? Посмотри на годболте во что атомарное чтение раскрывается.
На арме вроде тоже чтение бесплатно, а вот в записи барьер. Но я не изучал их модель.
Вон, я выше по ветке привёл реальный пример. В «mov eax, DWORD PTR p[rip]» раскрывается.
Подтверждаю.
Так читаешь код, и даже не уверен в его атомарности.
какой хардкор;)
В Йажа точно то же.
https://openjdk.java.net/jeps/171
кажется, что в реальной жизни это редко когда нужно
ну хотя в крестах же тоже вон борманд сказал, что можно мютексы юзать, и течь
В ваших крестах с блядскими перегрузочками нихера не понятно. Какие именно гарантии у этого чтения?
Пиши явно: while (!p.load(std::memory_order_relaxed)), тогда бесплатно. Для флажка об остановке релакса должно хватить.
Так а они ведь как-то видят изменения volatile-переменных после прерываний.
Прерывание происходит на том же самом ядре, со своим собственным кешем проблем не будет.
А для MMIO с железками как правило write back отключен, поэтому проц всё честно пишет.