Хотелось бы сделать несколько комментариев. Ссылки на стандарт ниже относятся к draft 4861 - это последний перед принятием официального C++20, который от него не должен отличаться. 1) 14:15 Acquire и release операции даны в стандарте в терминах "sequenced before" и "synchronize with", и там нет упоминаний (я не нашёл?) по поводу особого отношения к операция чтения/записи. Потому, раз вы предпочитатете объяснять на переупорядочивании, то правильнее будет говорить, что acquire запрещает переупорядочивать до, а release - после, и операции чтения, и операции записи, а не отдельно acquire - чтения, а release - записи, как сказано на слайде. 2) 16:45 Этот код содержит (внезапно!) неопределённое поведение. В соответствии с 16.5.4.11/1 нарушение предусловий функций стандартной библиотеки ведёт к неопределённому поведению. Теперь посмотрим на 31.8.1/6 и 31.8.1/12: для store параметр order не может быть memory_order::consume, memory_order::acquire или memory_order::acq_rel, а для load - memory_order::release или memory_order::acq_rel. Да, не все memory_order подходят для всех атомарных операций. 3) 29:41 Нет, эти две модели памяти не для compare и swap, а для успешного и не успешного CAS соответственно. И это разные вещи. Ибо если следовать вашим словам, то вызовы compare_exchange_{weak,strong} герировали бы два барьера сразу. Более того, сходу я не могу придумать случая, когда требовался бы failure memory order более "жёсткий", чем success memory order. И в 31.8.1/23 указано, что при одном аргументе failure memory order будет идентичен переданному, за исключением acq_rel, который заменяется на acquire, и release, который заменяется на relaxed. Так что на ваш последующий вопрос в 1:12:40, когда вы говорите, что второй аргумент sequentially-consistent, и предлагаете указать до какой степени его можно ослабить, я отвечаю так: вы его уже ослабили до relaxed, и да, так можно (хотя весь остальной код я не проверял, потому можно только в том случае, если вы не ошиблись с success memory order). 4) 32:23 Нет, этот пример вполне может вывести обе строчки: в данном примере у нас нет никакой гарантии, что мы прочитаем из x именно true. Семантика acquire-release гарантирует нам видимый порядок, но не гарантирует, что записанное с release будет мгновенно видно в другом потоке. В конце концов, представим, что весь код Thread 2 исполнился раньше (что бы это ни значило без sequentially-consistent гарантий :) ), чем код Thread 1: в этом случае мы можем увидеть оба вывода, потому что именно release synchronize with acquire, но никак не наоборот. Собственно, это написано в 6.9.2/6. Более того, в 48:15 вы показываете в целом аналогичный пример даже с более "жёстким" memory order, и совершенно верно говорите, что мы может увидеть два нуля. Может, вы это специально сделали, ибо перед этим вы говорите слушателям, что проблема не только в переупорядочивании команд, но и в "видимости" памяти разными потоками? Но в таком случае стоило бы вернуться к примеру с барьерами и сказать, что на самом деле там он тоже не помогает. 5) 38:15 Я не думаю, что тут дело в нежелании процессора переупорядочивать. Вы ведь сами говорите, что на ARM используется LL/SC, а не просто CAS. И, конечно, это сделано признаком в кэш-линии, а у вас обе переменные рядом. Потому для начала я бы объявил и a, и b с alignas(std::hardware_destructive_interference_size). Не могу гарантировать, что сразу же получим wrong, но шансы гораздо выше. 6) 41:19 Всё же volatile не работает :) Ну, и не надо учить людей, что якобы существует "UB которое никогда не проявится" - на эту тему есть замечательная исптория (www.linux.org.ru/forum/development/14422428). Но мало того, что у вас неопределённое поведение из-за data race, у вас ещё и барьеры не работают. В 31.11/2-4 ясно сказано, что fences работают только с ассоциированной атомарной операцией, коей в коде нет. Не могу сказать, что это ведёт к неопределённому поведению, но к отсутствию барьера вполне может привести - на всё воля компилятора. 7) 42:31 Не понимаю, почему вы не показываете слушателям правильный код без data race. Мои пробы на godbolt дают такой же код, что и показанный вами. Всё же считаю, что показывать студентам код с неопределённым поведением следут только с пояснением, что так ни в коем случае делать нельзя, а не как рабочий вариант. 8) 1:10:25 Так и не понял для чего на делать store в теле конструктора, если конструкторы std::atomic вообще не являются атомарными операциями, и потому могут быть "переупорядочены" как угодно. Правда, в 31.8.1/3 нас предупреждают о возможной гонке с конструктором, но это не наш случай, ведь очевидно, что в другой поток должен передаваться указатель (ссылка) на уже сконструированную очередь, т.е. всё должно происходить с соответствующими барьерами. 9) Не думаю, что выравниванием std::vector что-то даёт в данном случае. Я бы предложил выровнять CellTy по кэш-линии: всё же там атомарный счётчик, нам ни к чему лишние синхронизации с другими ядрами (или даже конфликты LL/SC на ARM). Но, конечно, это зависит от размера T.
Блин, такой классный голос, подача и темы отличные! Если бы была нейронка, которая генерила мне лекции Константина Владимирова на произвольные темы, я бы согласился впасть в матрицу, чтобы пару месяцев только жрать, спать и смотреть лекции Константина Владимирова
Не совсем понял пример на 33:00. Почитал про atomic_thread_fence на cppreference, и там написано, что atomic_thread_fence с release запрещает всем операциям чтения/записи, написанным до барьера, переноситься дальше вызовов store после барьера. Acquire, как я понял, работает противоположно, т.е. запрещает операциям чтения/записи, написанным после барьера переноситься раньше вызовов load, написанных до барьера. На слайде же в Thread 1 под барьером нет вызовов store, а в Thread 2 над барьером нет вызовов load. Т.е., получается, барьеры вообще не влияют на порядок действий в данной ситуации?
У Федора Пикуса есть отличная книга "The Art of Writing Efficient Programs" где рассмотрены модели памяти подробно, интересная книжка. Спасибо большое за интересное видео.
1:20:20 Возможно, 20%-ый оверхед на любом количестве потоков возникает из-за того, что sleep(1ms) на самом деле делает паузу не ровно 1ms, а 1.2ms из-за дискретности таймера?
Есть вопрос, касающийся memory_order. Какова может быть причина того что memory order это не шаблоный параметр ароматных функций? Разве можно представить разумную программу, которая в зависимости от рантайма будет менять memory order у атомарных операций и при этом будет работать корректно? И что вообще компилятор будет делать с таким кодом, если там memory order не известен на этапе компиляции, воткнёт switch или просто всегда будет ставить seq_cst?
Это очень разумный и удивительно глубокий вопрос который как-то мимо меня проскочил. Я думаю (впрочем не уверен), что тут правильный ответ это композиция. Вы можете написать свою функцию которая принимает memory order и спокойно передать его в стандартную функцию. Если бы вам нужно было выводить для этого шаблонный параметр, вы бы были вынужденно соглашаться на расползание шаблонного параметра по вашему коду. Что может быть не опцией, если функция, например, виртуальная.
Константин, спасибо за лекцию. Понравилась мнемоника, я сам никак не мог запомнить. Предлагаю расширенную дурацкую мнемонику: "Релиз - не пиши вниз, Эквая - читай не взлетая" :D
Хотелось бы сделать несколько комментариев. Ссылки на стандарт ниже относятся к draft 4861 - это последний перед принятием официального C++20, который от него не должен отличаться.
1) 14:15 Acquire и release операции даны в стандарте в терминах "sequenced before" и "synchronize with", и там нет упоминаний (я не нашёл?) по поводу особого отношения к операция чтения/записи. Потому, раз вы предпочитатете объяснять на переупорядочивании, то правильнее будет говорить, что acquire запрещает переупорядочивать до, а release - после, и операции чтения, и операции записи, а не отдельно acquire - чтения, а release - записи, как сказано на слайде.
2) 16:45 Этот код содержит (внезапно!) неопределённое поведение. В соответствии с 16.5.4.11/1 нарушение предусловий функций стандартной библиотеки ведёт к неопределённому поведению. Теперь посмотрим на 31.8.1/6 и 31.8.1/12: для store параметр order не может быть memory_order::consume, memory_order::acquire или memory_order::acq_rel, а для load - memory_order::release или memory_order::acq_rel. Да, не все memory_order подходят для всех атомарных операций.
3) 29:41 Нет, эти две модели памяти не для compare и swap, а для успешного и не успешного CAS соответственно. И это разные вещи. Ибо если следовать вашим словам, то вызовы compare_exchange_{weak,strong} герировали бы два барьера сразу. Более того, сходу я не могу придумать случая, когда требовался бы failure memory order более "жёсткий", чем success memory order. И в 31.8.1/23 указано, что при одном аргументе failure memory order будет идентичен переданному, за исключением acq_rel, который заменяется на acquire, и release, который заменяется на relaxed. Так что на ваш последующий вопрос в 1:12:40, когда вы говорите, что второй аргумент sequentially-consistent, и предлагаете указать до какой степени его можно ослабить, я отвечаю так: вы его уже ослабили до relaxed, и да, так можно (хотя весь остальной код я не проверял, потому можно только в том случае, если вы не ошиблись с success memory order).
4) 32:23 Нет, этот пример вполне может вывести обе строчки: в данном примере у нас нет никакой гарантии, что мы прочитаем из x именно true. Семантика acquire-release гарантирует нам видимый порядок, но не гарантирует, что записанное с release будет мгновенно видно в другом потоке. В конце концов, представим, что весь код Thread 2 исполнился раньше (что бы это ни значило без sequentially-consistent гарантий :) ), чем код Thread 1: в этом случае мы можем увидеть оба вывода, потому что именно release synchronize with acquire, но никак не наоборот. Собственно, это написано в 6.9.2/6. Более того, в 48:15 вы показываете в целом аналогичный пример даже с более "жёстким" memory order, и совершенно верно говорите, что мы может увидеть два нуля. Может, вы это специально сделали, ибо перед этим вы говорите слушателям, что проблема не только в переупорядочивании команд, но и в "видимости" памяти разными потоками? Но в таком случае стоило бы вернуться к примеру с барьерами и сказать, что на самом деле там он тоже не помогает.
5) 38:15 Я не думаю, что тут дело в нежелании процессора переупорядочивать. Вы ведь сами говорите, что на ARM используется LL/SC, а не просто CAS. И, конечно, это сделано признаком в кэш-линии, а у вас обе переменные рядом. Потому для начала я бы объявил и a, и b с alignas(std::hardware_destructive_interference_size). Не могу гарантировать, что сразу же получим wrong, но шансы гораздо выше.
6) 41:19 Всё же volatile не работает :) Ну, и не надо учить людей, что якобы существует "UB которое никогда не проявится" - на эту тему есть замечательная исптория (www.linux.org.ru/forum/development/14422428). Но мало того, что у вас неопределённое поведение из-за data race, у вас ещё и барьеры не работают. В 31.11/2-4 ясно сказано, что fences работают только с ассоциированной атомарной операцией, коей в коде нет. Не могу сказать, что это ведёт к неопределённому поведению, но к отсутствию барьера вполне может привести - на всё воля компилятора.
7) 42:31 Не понимаю, почему вы не показываете слушателям правильный код без data race. Мои пробы на godbolt дают такой же код, что и показанный вами. Всё же считаю, что показывать студентам код с неопределённым поведением следут только с пояснением, что так ни в коем случае делать нельзя, а не как рабочий вариант.
8) 1:10:25 Так и не понял для чего на делать store в теле конструктора, если конструкторы std::atomic вообще не являются атомарными операциями, и потому могут быть "переупорядочены" как угодно. Правда, в 31.8.1/3 нас предупреждают о возможной гонке с конструктором, но это не наш случай, ведь очевидно, что в другой поток должен передаваться указатель (ссылка) на уже сконструированную очередь, т.е. всё должно происходить с соответствующими барьерами.
9) Не думаю, что выравниванием std::vector что-то даёт в данном случае. Я бы предложил выровнять CellTy по кэш-линии: всё же там атомарный счётчик, нам ни к чему лишние синхронизации с другими ядрами (или даже конфликты LL/SC на ARM). Но, конечно, это зависит от размера T.
Блин, такой классный голос, подача и темы отличные! Если бы была нейронка, которая генерила мне лекции Константина Владимирова на произвольные темы, я бы согласился впасть в матрицу, чтобы пару месяцев только жрать, спать и смотреть лекции Константина Владимирова
Спасибо! Получился замечательный цикл про Атомики!
Не совсем понял пример на 33:00. Почитал про atomic_thread_fence на cppreference, и там написано, что atomic_thread_fence с release запрещает всем операциям чтения/записи, написанным до барьера, переноситься дальше вызовов store после барьера. Acquire, как я понял, работает противоположно, т.е. запрещает операциям чтения/записи, написанным после барьера переноситься раньше вызовов load, написанных до барьера.
На слайде же в Thread 1 под барьером нет вызовов store, а в Thread 2 над барьером нет вызовов load. Т.е., получается, барьеры вообще не влияют на порядок действий в данной ситуации?
У Федора Пикуса есть отличная книга "The Art of Writing Efficient Programs" где рассмотрены модели памяти подробно, интересная книжка. Спасибо большое за интересное видео.
1:20:20 Возможно, 20%-ый оверхед на любом количестве потоков возникает из-за того, что sleep(1ms) на самом деле делает паузу не ровно 1ms, а 1.2ms из-за дискретности таймера?
Блестящая догадка!
На 33:00 всё ещё можем увидеть две строчки несмотря на барьеры, если оба потока сначала сделают store, а затем пойдут загружать значение
Если оба успели сделать store, выводить строчку не пойдёт ни один. Они же читают под антипредикатом. Барьеры тут гарантируют, что успели.
Есть вопрос, касающийся memory_order. Какова может быть причина того что memory order это не шаблоный параметр ароматных функций? Разве можно представить разумную программу, которая в зависимости от рантайма будет менять memory order у атомарных операций и при этом будет работать корректно? И что вообще компилятор будет делать с таким кодом, если там memory order не известен на этапе компиляции, воткнёт switch или просто всегда будет ставить seq_cst?
Это очень разумный и удивительно глубокий вопрос который как-то мимо меня проскочил. Я думаю (впрочем не уверен), что тут правильный ответ это композиция. Вы можете написать свою функцию которая принимает memory order и спокойно передать его в стандартную функцию. Если бы вам нужно было выводить для этого шаблонный параметр, вы бы были вынужденно соглашаться на расползание шаблонного параметра по вашему коду. Что может быть не опцией, если функция, например, виртуальная.
Пример про seq_cst точно такой же как для вывода надписей "x not y" до этого, но говорятся про них разные вещи
Возможно, чего-то не понял, но хотелось бы прояснения этого момента, если возможно
На 61 минуте лекции говорится про сайт, можно ссылку на него?
www.1024cores.net/
Константин, спасибо за лекцию. Понравилась мнемоника, я сам никак не мог запомнить.
Предлагаю расширенную дурацкую мнемонику:
"Релиз - не пиши вниз,
Эквая - читай не взлетая"
:D
После всего этого многопоточного C++ я начинаю завидовать программистам ПЛК.
Скукота, а в C++ всегда весело и интересно и это гарантируется самим языком!