В примере на 41:30 метод wait сначала проверяет предикат, то есть вызов void wait( std::unique_lock& lock, Predicate stop_waiting ); то же самое, что while (!stop_waiting()) { wait(lock); } а std::cout
В моей трассе первый из processing тредов добегает до cond_wait до вызова cond_signal, однако, что интересно, первый (самый ранний из трех) из processing тредов из conditional_variable::wait уходит вниз в pthread_cond_wait и дальше в futex_xxx_wait, а второй processing (самый поздний из трех) из conditional_variable::wait выходит сразу
Константин, спасибо за лекцию! 1:09:22 - то есть, шаред мутекс будет выгоден только тогда, когда под юник лок будет что-то тяжёлое, я правильно понимаю? Но этого же обычно не бывает. Там всегда что-то лёгкое. Выходит, лучше пользоваться обычным мутексом (после измерений, конечно же :) ) ?
Лучше вообще не использовать мьютексы в явном виде. Но если приходится, то да, лучше обычные. Хороший пример где шареный мьютекс был бы измеримо и безальтернативно лучше я пока не нашёл.
Константин Игоревич, спасибо за лекцию! А как насчет парочки доп. семинаров - факультативов по оптимизации с использованием VTune и подобного? Было бы просто замечательно :)
13:33 да вроде не ломается. UB есть в первой проверке по синей стрелке. Допустим, 100 первых потоков из миллиона проходят первый if(!resptr) , а мютекс захватил только 1 из них, остальные 99 ждут освобождения или ещё читают первое условие. Этот 1-й поток проходит 2-ю проверку, создаёт ресурс и уходит. Остальные 99 по очереди входят в критическую секцию и на 2-м ифе видят, что ресурс уже создан... И где здесь UB? :) Если вставить какой-то код между первой проверкой и lock_guard тогда плохо, но это уже будет не DCL, как я понимаю. UPD: тогда выходит, что проблема может быть в 101-м потоке? 1-й поток записал resptr частично, но этого хватило, чтоб 101-й уже не прошёл 1-ю проверку, но не хватило, чтоб вызвать функцию use() корректно? Если const функция, например, читает состояние, а его там нет.
@@tilir как вариант при двух. Вообще, здесь всего 3 кейса: 1) Второй процесс прошёл первую проверку и ждёт, пока первый пишет. Т.е. второй успел запрыгнуть в первый иф до мютекса. Случай проблемный в плане времени ожидания, если процессов много. Но в эту зону ожидания попадет некий % от всех , что лучше, чем если ждать будут все. 2) Второй не прошел первую проверку, т.к. первый уже успел записать. Тут всё хорошо. Чем больше % таких процессов, тем быстрее общая работа. 3) Второй проходит проверку, когда первый пишет. Это главная проблема, да? До того, как 1-й начнёт писать, конструктор уже отработал и вывел "c". А потом 1-й пишет в resptr. И, что бы он туда не записал, второй это преобазует в бул при проверке и проскакиевает сразу к resptr -> use(). А дальше завист от того, читает ли константная resptr -> use() внтуреннее состояние объекта, которое либо уже есть, если повезло, либо нет, тогда segmentation fault какой-нибудь. В ваших примерах resptr -> use() состояние не читает, поэтому они всегда будут работать коректно и выводить "u" после выведеного ранее "c", даже при oxрелиарде тестов. Т.е. чтоб был шанс сломать, надо чтоб use() читал какое-то поле объекта хотя бы. Вроде так.
@@test_bot5541 а вдруг в третем кейсе эксепшн, отключится питание или сразу же позвонит автор компилятора и попросит больше так не делать? Где гарантия, что читатель преобразует в bool то, что пишется?
Кажется проблема может быть в случае если запись указателя не атомарна. Я пытался повторить такую штуку с невыровнеными указателями (через pragma pack(1)), но даже в таких условиях получить проблемы не удалось. Мне представляется допустимый с теоретической точки зрения следующий сценарий выполнения: 1) Поток один выделил память и инициализировал объект, и начал записывать адрес в указатель, но эта операция не атомарна и скажем он успел записать только 4 байта из 8. 2) Поток два входит в метод, делает первую проверку, там уже не nullptr, поэтому он идет дальше без мьютекса и вызывает метод с мусорным указателем. Соответсвенно если это виртуальный метод, или внутри любое обращение к данным, то получаем SegFault. Добавлю, что в описанном мной сценарии я исхожу из того, что сначала должен отработать конструктор, а уже потом будет выполнено присваивание. Я не до конца уверен, что в потоке один можно полностью исключить ситуацию, когда сначала будет выделена память, она присвоится указателю и только потом на этой памяти будет вызван конструктор. Не говоря уже про возможные проблемы с кешами и реордерингами, в которых я довольно поверхностно что-то понимаю. Согласен, формально это UB, тред санитайзер выдает предупреждение (в двух местах - в первом чтении перед локом и при обращении после проверки), но вот лично у меня сломать не получилось ни на arm, ни на x86.
Про shared_mutex стало понятно, что при малом размере критической секции он сильно проигрывает обычному мьютексу. Но есть ли достойная альернатива для подобного типа синхронизации?
у меня вот концептуальный вопрос назрел -- а могут ли компиляторы обладать описанием, что они предоставляют возможность компиляции С++ кода, если они настолько часто плюют на стандарт? -- ведь если именно валидная программа согласно стандарту не работает должным образом при ее компиляции компилятором -- то компилятор компилирует по правилам, отличным от стандарта с++... Тоесть по факту компилятор не с++, а собственного диалекта с некоторыми оговорками. В таком случае юридически они обязаны не называться компиляторами с++.
Они не плюют. Если вы идёте и доказываете что стандарт где-то нарушен это обычно рассматривается как дефект и его чинят. Я бы сказал что компиляторы ведут себя ЛУЧШЕ чем разработчики, которым иногда говоришь: вот у тебя UB, перепиши. И дальше несколько итераций "ну всё же работает" проламывания.
Отличная лекция, очень интересно. Крутой, заинтересованный преподаватель. Спасибо)
Большое спасибо за лекцию! Очень интересно
Вдруг не видели, есть книга : Параллельное программирование на современном С++, 2022 ,Райнер Гримм. Читается полегче, чем Энтони Вильямс.
спасибо! Гримма еще не читал, попробую, но хочется сказать, что в любом случае Уильямс все равно хорош и достоен рекомендации :)
Прочитал коммент и моментально убежал покупать, пока есть в наличии; спасибо за рекомендацию!
В примере на 41:30 метод wait сначала проверяет предикат, то есть вызов
void wait( std::unique_lock& lock, Predicate stop_waiting );
то же самое, что
while (!stop_waiting())
{
wait(lock);
}
а std::cout
Фокус в том как это проверить. В итоге инструменты есть например снять трассу и посмотреть. Но они на удивление мало известны. Хотя казалось бы.
В моей трассе первый из processing тредов добегает до cond_wait до вызова cond_signal, однако, что интересно, первый (самый ранний из трех) из processing тредов из conditional_variable::wait уходит вниз в pthread_cond_wait и дальше в futex_xxx_wait, а второй processing (самый поздний из трех) из conditional_variable::wait выходит сразу
Под wsl perf можно вроде бы просто собрать, исходники есть на гитхабе в WSL2-Linux-Kernel/tools/perf. Но пока собирать не пробовал.
Константин, спасибо за лекцию!
1:09:22 - то есть, шаред мутекс будет выгоден только тогда, когда под юник лок будет что-то тяжёлое, я правильно понимаю?
Но этого же обычно не бывает. Там всегда что-то лёгкое.
Выходит, лучше пользоваться обычным мутексом (после измерений, конечно же :) ) ?
Лучше вообще не использовать мьютексы в явном виде. Но если приходится, то да, лучше обычные. Хороший пример где шареный мьютекс был бы измеримо и безальтернативно лучше я пока не нашёл.
@@tilir понятно. Интересно, зачем его такой вводили тогда в язык
@@alex_s_ciframi ну вроде в POSIX тоже есть. Значит кому-то нужен.
Константин Игоревич, спасибо за лекцию! А как насчет парочки доп. семинаров - факультативов по оптимизации с использованием VTune и подобного? Было бы просто замечательно :)
Возможно, забегаю вперёд, но интересно, есть ли в плане лекций корутины?
Да, в конце. В этом курсе в отличие от прошлого внутри корутин будет многопоточность.
13:33 да вроде не ломается. UB есть в первой проверке по синей стрелке. Допустим, 100 первых потоков из миллиона проходят первый if(!resptr) , а мютекс захватил только 1 из них, остальные 99 ждут освобождения или ещё читают первое условие. Этот 1-й поток проходит 2-ю проверку, создаёт ресурс и уходит. Остальные 99 по очереди входят в критическую секцию и на 2-м ифе видят, что ресурс уже создан... И где здесь UB? :) Если вставить какой-то код между первой проверкой и lock_guard тогда плохо, но это уже будет не DCL, как я понимаю.
UPD: тогда выходит, что проблема может быть в 101-м потоке? 1-й поток записал resptr частично, но этого хватило, чтоб 101-й уже не прошёл 1-ю проверку, но не хватило, чтоб вызвать функцию use() корректно? Если const функция, например, читает состояние, а его там нет.
Проблема уже при двух потоках. Первый пишет под критической секцией, второй читает до входа в неё.
@@tilir как вариант при двух. Вообще, здесь всего 3 кейса:
1) Второй процесс прошёл первую проверку и ждёт, пока первый пишет. Т.е. второй успел запрыгнуть в первый иф до мютекса. Случай проблемный в плане времени ожидания, если процессов много. Но в эту зону ожидания попадет некий % от всех , что лучше, чем если ждать будут все.
2) Второй не прошел первую проверку, т.к. первый уже успел записать. Тут всё хорошо. Чем больше % таких процессов, тем быстрее общая работа.
3) Второй проходит проверку, когда первый пишет. Это главная проблема, да? До того, как 1-й начнёт писать, конструктор уже отработал и вывел "c". А потом 1-й пишет в resptr. И, что бы он туда не записал, второй это преобазует в бул при проверке и проскакиевает сразу к resptr -> use(). А дальше завист от того, читает ли константная resptr -> use() внтуреннее состояние объекта, которое либо уже есть, если повезло, либо нет, тогда segmentation fault какой-нибудь. В ваших примерах resptr -> use() состояние не читает, поэтому они всегда будут работать коректно и выводить "u" после выведеного ранее "c", даже при oxрелиарде тестов. Т.е. чтоб был шанс сломать, надо чтоб use() читал какое-то поле объекта хотя бы. Вроде так.
Если происходит UB, мы уже не имеем права предполагать какое то конкретное поведение связанное с ним. Всё может пойти как угодно.
@@test_bot5541 а вдруг в третем кейсе эксепшн, отключится питание или сразу же позвонит автор компилятора и попросит больше так не делать? Где гарантия, что читатель преобразует в bool то, что пишется?
Кажется проблема может быть в случае если запись указателя не атомарна. Я пытался повторить такую штуку с невыровнеными указателями (через pragma pack(1)), но даже в таких условиях получить проблемы не удалось. Мне представляется допустимый с теоретической точки зрения следующий сценарий выполнения:
1) Поток один выделил память и инициализировал объект, и начал записывать адрес в указатель, но эта операция не атомарна и скажем он успел записать только 4 байта из 8.
2) Поток два входит в метод, делает первую проверку, там уже не nullptr, поэтому он идет дальше без мьютекса и вызывает метод с мусорным указателем. Соответсвенно если это виртуальный метод, или внутри любое обращение к данным, то получаем SegFault.
Добавлю, что в описанном мной сценарии я исхожу из того, что сначала должен отработать конструктор, а уже потом будет выполнено присваивание. Я не до конца уверен, что в потоке один можно полностью исключить ситуацию, когда сначала будет выделена память, она присвоится указателю и только потом на этой памяти будет вызван конструктор. Не говоря уже про возможные проблемы с кешами и реордерингами, в которых я довольно поверхностно что-то понимаю.
Согласен, формально это UB, тред санитайзер выдает предупреждение (в двух местах - в первом чтении перед локом и при обращении после проверки), но вот лично у меня сломать не получилось ни на arm, ни на x86.
Про shared_mutex стало понятно, что при малом размере критической секции он сильно проигрывает обычному мьютексу. Но есть ли достойная альернатива для подобного типа синхронизации?
Рассмотрите замену мьютекса на очередь. Локфри очереди неплохи в ситуации редких записей и частых чтений.
как всегда выше всяких похвал. большое спасибо за ваши труды!
у меня вот концептуальный вопрос назрел -- а могут ли компиляторы обладать описанием, что они предоставляют возможность компиляции С++ кода, если они настолько часто плюют на стандарт? -- ведь если именно валидная программа согласно стандарту не работает должным образом при ее компиляции компилятором -- то компилятор компилирует по правилам, отличным от стандарта с++... Тоесть по факту компилятор не с++, а собственного диалекта с некоторыми оговорками. В таком случае юридически они обязаны не называться компиляторами с++.
Они не плюют. Если вы идёте и доказываете что стандарт где-то нарушен это обычно рассматривается как дефект и его чинят. Я бы сказал что компиляторы ведут себя ЛУЧШЕ чем разработчики, которым иногда говоришь: вот у тебя UB, перепиши. И дальше несколько итераций "ну всё же работает" проламывания.