Нашли или выдавили из себя код, который нельзя назвать нормальным,
на который без улыбки не взглянешь?
Не торопитесь его удалять или рефакторить, — запостите его на
говнокод.ру, посмеёмся вместе!
// https://deadlockempire.github.io/
// Игра, где надо играть за планировщик чтоб вызвать дедлок
// https://deadlockempire.github.io/#2-flags
// First Army
while (true) {
while (flag != false) {
;
}
flag = true;
critical_section();
flag = false;
}
// Second Army
while (true) {
while (flag != false) {
;
}
flag = true;
critical_section();
flag = false;
}
The day finally came. The Deadlock Empire opened its gates and from them surged massive amounts of soldiers, loyal servants of the evil Parallel Wizard. The Wizard has many strengths - his armies are fast, and he can do a lot of stuff that we can't. But now he set out to conquer the world, and we cannot have that.
You are our best Scheduler, commander! We have fewer troops and simpler ones, so we will need your help. Already two armies of the Deadlock Empire are approaching our border keeps. They are poorly equipped and poorly trained, however. You might be able to desync them and break their morale.
volatile запрещает оптимизировать обращения, но барьеров не будет. Поэтому critical_section() может выплыть из-под "мутекса". Если заинлайнится, конечно.
Атомик втыкает барьеры, но не запрещает оптимизировать обращения. Они могут слипнуться если это никому не ломает happens before.
> Если гонка с железом или прерываниями, то volatile.
Допустим есть микроконтроллер с одним ядром, есть некая глобальная volatile переменная, которая может читаться/писаться в обработчике прерывания и в обычном коде. Допустим, переменная там 64-битная, а инструкции записи байтиков в память есть максимум на 32 бит, и вот в коде мы наполовину перезаписали volatile переменную новым значением, и тут хуяк - обработчик прерывания. А в переменной вообще хуйня какая-то непредусмотренная
Ну в атомарных интринсиках обычно обе защиты есть: конпеляторный барьер и процессорный барьер.
Но, насколько я помню, для x.store(42); x.store(100500) конпелятор и проц имеют право выкинуть первую инструкцию. Порядок это не ломает, сторонний наблюдатель просто подумает, что он пиздец невезучий, раз никогда не видит там 42.
> ... That is all we need for what volatile is intended for: manipulating I/O registers or memory-mapped hardware, but it doesn't help us in multithreaded code where the volatile object is often only used to synchronize access to non-volatile data. Those accesses can still be reordered relative to the volatile ones.
volatile вообще нехуй так использовать, компилятор имеет право всякое говно между этими volatile переставлять
А 64-битный атомик на 32-битном ядре повиснет нахуй если он через спинлок сэмулирован, лол. Тоже не лучшее решение для расшаривания данных с прерыванием.
Можно придумать довольно-таки ебанутый способ расшарить данные с прерыванием. Допустим, есть 64-битная глобалка, которую надо читать/писать и из прерывания и обычным способом, а инструкции записи/чтения байтиков есть только 32-битные. Тогда можно в обработчике прерывания узнавать, из какого места сработало прерывание, и если оно сработало как раз тогда, когда из глобалки байтики читались и записывались в какие-то регистры (и байтики недопереписались), то тогда само прерывание должно там доделать, т.е. дозаписать байтики в регистры треда(т.е. сохраненный стейт треда), который был прерван прерыванием, и передвинуть instruction poiner на те инструкции, которые идут после инструкций записи байтиков из глобалки в регистры общего назначения.
И потом дальше делать как обычно.
> 8.1.1 Guaranteed Atomic Operations
>
> The Intel486 processor (and newer processors since) guarantees that the following basic memory operations will always be carried out atomically:
>
> •Reading or writing a byte
> •Reading or writing a word aligned on a 16-bit boundary
> •Reading or writing a doubleword aligned on a 32-bit boundary
>
> The Pentium processor (and newer processors since) guarantees that the following additional memory operations will always be carried out atomically:
>
> •Reading or writing a quadword aligned on a 64-bit boundary
> •16-bit accesses to uncached memory locations that fit within a 32-bit data bus
>
> The P6 family processors (and newer processors since) guarantee that the following additional memory operation will always be carried out atomically:
>
> •Unaligned 16-, 32-, and 64-bit accesses to cached memory that fit within a cache line
т.е. никакого xadd не надо для атомарности инкремента, если адрес записи выровнен по нужной границе
Ну да, это скорее что он не прочитает половинку старых данных и половину новых. Как это может произойти на границе кешлайна для криво выравненной переменной.
Емнип, на практике не атомарный add ещё как плющит из нескольких потоков.
Тут еще такой момент: если два треда инкрементируют/декрементируют одну и ту же глобалку, тут понятно что нужен атомик, но вот если есть один обычный "тред" и прерывание (как в каком-нибудь контроллере, и нихуя нет планировщика процессов как такового), то атомарные инструкции достаточно использовать только для "треда", потому как прерывание никто на полпути не прервет (если нет каких-то других прерываний, которые имеют более высокий приоритет, и которые в ту же глобалку лезут) так что в прерывании использование атомарной модификации избыточно. В прерывании даже volatile не требуется
Ну если ты уверен, что тебя не вытеснит более важное прерывание, то ок.
Понятное дело, что обычный поток кода не может выполняться, пока выполняется прерывание в одноядерной системе) Это я еще со времен доса помню.
А как именно код и обработчик прерывания достукиваются до этой глобалки?
Если это просто какое-то место в памяти, то разве не нужно его поменчать как volatile? компилятор же выкинет иначе нафиг всё, нет?
Мы ж про глобалки говорим, а не про какие-то переменные в скоупе. Глобалки должны поменяться, когда выходим из функции, которая какие-то глобалки поменяла.
то если в вызов shit3 заинлайнивается вся хуйня и компилятор это заоптимизирует, то запись в glob_var там будет только одна, ибо нехуй лишний раз ворошить память. И если shit3() это какой-то обработчик прерываний, то "применить" изменения глобалок надо тогда, когда весь этот обработчик завершится, так что по итогу там будет 1337 и никакие предыдущие записи в глобалку нахуй не нужны, раз обработчик никто на полпути не прервет. Но если glob_var это какая-то хуйня типа MMIO и там особая хуйня должна происходить при записи говна по таким-то адресам, то тогда вот можно volatile юзать
Да не, выкидывается только код, который не имеет полезных сайд-эффектов.
Если насрать в глобалку и выйти -- конпелятор обязан сохранить такой эффект. Он даже отложить его на попозже его не может т.к. ISR никуда не инлайнится.
>Ну если ты уверен, что тебя не вытеснит более важное прерывание, то ок.
Оно вполне может вытеснять, важно чтоб оно не конфликтовало, т.е. не ворошило те же самые глобалки, с которыми то прерывание имеет дело.
guest # 0
армия:строка
1:9
2:20
1:12
2:23
1:13
2:24
еблысь
guest # 0 ⇈
если во круг флага нет заборов, то он по идее может остаться в буфере ЦПУ, и в теории армия1 вообще никогда не узнает, что его изменила армия2.
bormand # 0 ⇈
Да и flag = true с flag = false может слипнуться ещё во время конпеляции.
З.Ы. Угу, gcc от всей этой хуйни только внешний while (1) оставил.
MAPTbIwKA # 0 ⇈
volatile нужно пхать, да?
bormand # 0 ⇈
guest # 0 ⇈
я думал, что это volatile.
Срать может как и другое ядро, так и железо. просто в случае с другим ядром этого НЕДОСТАТОЧНО, потому что код-то он не выкинет, но гонка останется
или нет?
bormand # 0 ⇈
Атомик втыкает барьеры, но не запрещает оптимизировать обращения. Они могут слипнуться если это никому не ломает happens before.
MAPTbIwKA # 0 ⇈
Я имел ввиду, что volatile хоть и не сделает код корректным, но гарантирует, что его хотя-бы не выкинут.
Потому что без волатайла тут написано ``while(true){}``
Разумеется, обращение к переменной из двух потоков нужно синхронизировать барьером.
CEHT9I6PbCKuu_nemyx # 0 ⇈
j123123 # 0 ⇈
Допустим есть микроконтроллер с одним ядром, есть некая глобальная volatile переменная, которая может читаться/писаться в обработчике прерывания и в обычном коде. Допустим, переменная там 64-битная, а инструкции записи байтиков в память есть максимум на 32 бит, и вот в коде мы наполовину перезаписали volatile переменную новым значением, и тут хуяк - обработчик прерывания. А в переменной вообще хуйня какая-то непредусмотренная
MAPTbIwKA # 0 ⇈
Выходит, что запись в такую переменную это критическая секция, и нужно маскировать прерыввания на время записи, не?
bormand # 0 ⇈
MAPTbIwKA # 0 ⇈
У нас в жабке эти две концепции слиты воедино, нам проще
bormand # 0 ⇈
Но, насколько я помню, для x.store(42); x.store(100500) конпелятор и проц имеют право выкинуть первую инструкцию. Порядок это не ломает, сторонний наблюдатель просто подумает, что он пиздец невезучий, раз никогда не видит там 42.
j123123 # 0 ⇈
> ... That is all we need for what volatile is intended for: manipulating I/O registers or memory-mapped hardware, but it doesn't help us in multithreaded code where the volatile object is often only used to synchronize access to non-volatile data. Those accesses can still be reordered relative to the volatile ones.
volatile вообще нехуй так использовать, компилятор имеет право всякое говно между этими volatile переставлять
https://godbolt.org/z/Pad3n5
- вот например оно два инкремента не-volatile глобальной переменной спихнуло в одну инструкцию после всей волатильной питушни
bormand # 0 ⇈
MAPTbIwKA # 0 ⇈
например
volatile_crap++
нельзя свернуть в volatile_crap += 2
а регулярный можно.
j123123 # 0 ⇈
И потом дальше делать как обычно.
guest # 0 ⇈
bormand # 0 ⇈
guest # 0 ⇈
bormand # 0 ⇈
interrupt_safe_atomic<uint64_t> x;
x = 42;
bormand # 0 ⇈
MAPTbIwKA # 0 ⇈
А если у CPU есть атомарная инструкция для этого?
bormand # 0 ⇈
MAPTbIwKA # 0 ⇈
В жабке есть AtomicInteger например
bormand # 0 ⇈
MAPTbIwKA # 0 ⇈
это std::atomic дает такую гарантию?
Потому что если я просто использую атомарную иструацию цепеу, то мне это не поможет.
PetuhCountAtomicIncrement()
SendDataToIoPort()
PetuhCountAtomicIncrement()
если питух не волатилен, то что мешает компилятору сделать
PetuhCountAtomicIncrement()
PetuhCountAtomicIncrement()
SendDataToIoPort()
?
bormand # 0 ⇈
j123123 # 0 ⇈
> 8.1.1 Guaranteed Atomic Operations
>
> The Intel486 processor (and newer processors since) guarantees that the following basic memory operations will always be carried out atomically:
>
> •Reading or writing a byte
> •Reading or writing a word aligned on a 16-bit boundary
> •Reading or writing a doubleword aligned on a 32-bit boundary
>
> The Pentium processor (and newer processors since) guarantees that the following additional memory operations will always be carried out atomically:
>
> •Reading or writing a quadword aligned on a 64-bit boundary
> •16-bit accesses to uncached memory locations that fit within a 32-bit data bus
>
> The P6 family processors (and newer processors since) guarantee that the following additional memory operation will always be carried out atomically:
>
> •Unaligned 16-, 32-, and 64-bit accesses to cached memory that fit within a cache line
т.е. никакого xadd не надо для атомарности инкремента, если адрес записи выровнен по нужной границе
j123123 # 0 ⇈
bormand # 0 ⇈
Емнип, на практике не атомарный add ещё как плющит из нескольких потоков.
j123123 # 0 ⇈
MAKAKA # 0 ⇈
Понятное дело, что обычный поток кода не может выполняться, пока выполняется прерывание в одноядерной системе) Это я еще со времен доса помню.
А как именно код и обработчик прерывания достукиваются до этой глобалки?
Если это просто какое-то место в памяти, то разве не нужно его поменчать как volatile? компилятор же выкинет иначе нафиг всё, нет?
j123123 # 0 ⇈
По адресу памяти, очевидно.
> Если это просто какое-то место в памяти, то разве не нужно его поменчать как volatile?
Нет, зачем?
> компилятор же выкинет иначе нафиг всё, нет?
С чего б ему выкидывать что-то?
guest # 0 ⇈
*p = 1;
он видит, что автматическая переменная p нигде не испльзуется, и выкидывает ее
или нет?
j123123 # 0 ⇈
j123123 # 0 ⇈
то запись байтика по адресу 0xAABBCCDD у тебя нихуя выкидываться не будет
j123123 # 0 ⇈
то если в вызов shit3 заинлайнивается вся хуйня и компилятор это заоптимизирует, то запись в glob_var там будет только одна, ибо нехуй лишний раз ворошить память. И если shit3() это какой-то обработчик прерываний, то "применить" изменения глобалок надо тогда, когда весь этот обработчик завершится, так что по итогу там будет 1337 и никакие предыдущие записи в глобалку нахуй не нужны, раз обработчик никто на полпути не прервет. Но если glob_var это какая-то хуйня типа MMIO и там особая хуйня должна происходить при записи говна по таким-то адресам, то тогда вот можно volatile юзать
3_dar # 0 ⇈
MAKAKA # 0 ⇈
автоматические переменные никто не видит, но так как память-то у нас не в стеке, то запись в нее нельзя выкидывать
выкинуть
*a = 51;
он может только если потом идет
*a = 52
и тут нужен волатил
bormand # 0 ⇈
Если насрать в глобалку и выйти -- конпелятор обязан сохранить такой эффект. Он даже отложить его на попозже его не может т.к. ISR никуда не инлайнится.
MAKAKA # 0 ⇈
запись в глобалку нельзя отменить, можно только схлопнуть две записи
j123123 # 0 ⇈
Оно вполне может вытеснять, важно чтоб оно не конфликтовало, т.е. не ворошило те же самые глобалки, с которыми то прерывание имеет дело.
Desktop # 0 ⇈
bormand # 0 ⇈
Desktop # 0 ⇈
bormand # 0 ⇈
Desktop # 0 ⇈
guest # 0 ⇈
Desktop # 0 ⇈
bormand # 0 ⇈
В том же "uefi" таймерное прерывание может само себя прервать 4-5 раз. Такие вот дела.
MAKAKA # 0 ⇈
bormand # 0 ⇈
MAPTbIwKA # 0 ⇈
понятно
bormand # 0
Congratulations, you have triggered deadlocks in same way normal CPUs do!
hormand # 0
hormand # 0 ⇈
hormand # 0 ⇈
3.14159265 # 0