Вопрос из форума: Небольшое лирическое отступление. Если В() будет запущен после А(), то будет напечатано 123 по-определению. В распределенных системах без глобального времени понятие "после" как раз и вводится как — если в одной точке пространства мы увидели событие, произошедшее во второй точке пространства, то момент наблюдения произошёл *после* события. Других способов определить понятие *после* просто нет. Поэтому если В() после А(), то будет напечатано 123. Если 123 не будет напечатано, значит В() просто был не после А(). Я думаю, что подразумевался вопрос — если будет что-то напечатано, то гарантировано ли, что это будет 123? Правильных вариантов тут нет. Во-первых, основное правило касательно барьеров памяти — это всегда игра двух. Если один поток хаотически замешивает свои обращения к памяти в кучу-малу, то как бы ни старался второй поток установить корректный порядок, он не сможет это сделать(*). Например, если первый поток будет скомпилирован как: и при этом он будет прерван ОС после выполнения _complete = true, несложно понять, что как бы второй поток не старался, он не сможет гарантировать _answer==123, после того как он увидел _complete==true. Поэтому все варианты, где только один поток выполняет барьер можно сразу исключить. Второй момент — барьеры памяти обеспечивают взаимное упорядочивание обращений к памяти, поэтому они должны ставится *между* критическими обращениями (теми, которые мы хотим упорядочить), а не просто где-то. Критические обращения в данном случае — это обращения к _complete и _answer. Барьеры памяти должны быть *между* ними. Пока эти обращения находятся по одну сторону барьера, барьер имеет на них ровно нулевое влияние. Этим мы исключаем все остальные варианты. В целом, это один из основополагающих паттернов межпоточной синхронизации, в частности он используется в очередях производитель-потребитель, да и во всех остальных контейнерах. Правильно он записывается следующим образом: Теперь всё на своих местах. Write Barrier в потоке А гарантирует, что _complete будет установлен после установки _answer. Read Barrier в потоке В гарантирует, что _answer будет считан после _complete. Из этого можно заключить, что если В увидел _complete==true, то считывание _answer гарантированно выдаст 123. QED. Вообще говоря, нотация Read/Write барьеров считается несколько устаревшей. Сейчас считается более "продвинутой" нотация Acquire/Release барьеров, которая как раз поддерживается CLI volatile переменными. Т.е. любое считывание volatile переменной есть load с acquire барьером, а любая запись есть store с release барьером. Причин к этому много сложных, но вот например самая очевидная. Вообще говоря потоку В обычно надо упорядочить не только считывания данных, но и записи в синхронизируемые данные, т.е. поток В зачастую выглядит как: Read Barrier обеспечивает корректное упорядочивание считывания из _answer, но не упорядочивает запись в _answer. Соотв. мы может получить следующий результат: на экран будет выведено 123, но в переменной _answer будет лежать не 456, а 123. К чему я это. Если уж речь идёт о CLI/.NET, то более корректный и простой способ записать этот паттерн будет объявление переменной _complete как volatile, всё остальное будет обеспечено автоматически: Если не вдаваться в детали, то store-release гарантирует, что все *предыдущие* обращения к памяти будут завершены ДО данной операции. load-acquire соотв. гарантирует, что все *последующие* обращения к памяти будут выполнены ПОСЛЕ данной операции. При этом они не различают обращения к памяти на read и write. --- (*) Вообще говоря есть способ избавить один поток от выполнения аппаратного барьера памяти, но даже он требует выполнения барьера компилятора в обоих потоках. Такая синхронизация называется asymmetric synchronization. |