Re[3]: Доступ к локальной переменной из разных потоков
От: vmpire Россия  
Дата: 17.11.20 19:08
Оценка:
Здравствуйте, VladD2, Вы писали:


V>>Если переменная не volatile и нет лока вокруг, то чтение, вероятно, может быть соптимизировано и читающий поток не получит нового значения.

VD>На практике это не так и MS поддерживает это негласное соглашение, так как в ином случае куча программ перестанет работать. Это вам не плюсы.
Ну я в шарп кога-то как раз с плюсов пришёл, вот и опасаюсь по привычке.
Но мне кажется, это как-то неправильно исключать какие-то оптимизации из опасений что развалятся программы, написанные с опасными допущениями.
Тогда уж лучше по честному. как в js объявить, что у нас всё однопоточное, кроме специальных расширений с оговоренными механизмами синхронизации.
Отредактировано 17.11.2020 19:10 vmpire . Предыдущая версия .
Re[4]: Доступ к локальной переменной из разных потоков
От: VladD2 Российская Империя www.nemerle.org
Дата: 17.11.20 20:34
Оценка:
Здравствуйте, netch80, Вы писали:

N>Ну влепил долго работающий код, и что?


И все. Вполне реалистичный пример.

N>Это только из примера этого кода. А если не зацикливаться на нём?


То думать нужно в каждом конкретном случае.

N>OK, сработает. Для такой задачи проблем не будет. Значит ли это, что так будет всегда и везде?


Универсальных решений нет. Лично я вообще считаю, что с потоками луче работать через Акторы.

Просто не надо объявлять непригодным совершенно конкретный случай. Если понимаешь что делаешь, проблем не будет.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[2]: Доступ к локальной переменной из разных потоков
От: Aquilaware  
Дата: 17.11.20 23:50
Оценка: 245 (10) +1
Здравствуйте, VladD2, Вы писали:

VD>Дотнет не гарантирует атомарности и корректного разделение переменных и полей, но поле типа bool и целые, на практике, работают атомарно и корректно.


Дотнет гарантирует атомарность всех примитивных типов у которых sizeof(T) <= IntPtr.Size. Это прописано в спецификации .NET Memory Model.

VD>Проблемы могут начаться, если нужна точная и быстрая реакция на изменение значения. Какое-то время разные процессоры будут иметь свои копии. И изменение переменной в одном потоке не сразу попадет в кэш другого. Но в твоем примере это не важно. Ну, выйдет второй поток не через 5 секунд, а через 5 целых и 1 сотую секунды.


Для x86 синхронизация между кешами данных происходит при: 1) Memory Barrier 2) автоматически каждые 10 нс

Не все архитектуры CPU делают автоматическую синхронизацию кешей данных. Поэтому правильное решение состоит в указании ключевого слова volatile для переменной.

Но поскольку указать volatile для переменной обьявленной "локально" нельзя с точки зрения синтаксиса, нужно использовать методы Volatile.Read и Volatile.Write.

Сначала продемонстрирую вырожденный случай:

static void Main()
{
    bool b = true;

    new Thread(
        () =>
        {
            Thread.Sleep(1000);
            b = false;
        })
    {
        IsBackground = true
    }
    .Start();

    while (b)
    {
        //Console.WriteLine("b=" + b);
        //Thread.Sleep(100);
    }

    Console.WriteLine("b=" + b);
}


Этот пример зависает навсегда если собрать его в Release конфигурации под .NET 5.0.

Обратите внимание, что я закоментировал Console.WriteLine и Thread.Sleep. Почему? Потому что внутри эти операции используют примитивы синхронизации ОС, а они подразумевают неявный memory barrier. Например, документация Windows это декларирует явно.

Теперь заставим этот пример работать как надо. Например, так:

static void Main()
{
    bool b = true;

    new Thread(
        () =>
        {
            Thread.Sleep(1000);
            Volatile.Write(ref b, false);
        })
    {
        IsBackground = true
    }
    .Start();

    while (Volatile.Read(ref b))
    {
        //Console.WriteLine("b=" + b);
        //Thread.Sleep(100);
    }

    Console.WriteLine("b=" + b);
}


Или так:

static void Main()
{
    bool b = true;

    new Thread(
        () =>
        {
            Thread.Sleep(1000);

            Thread.MemoryBarrier();
            b = false;
        })
    {
        IsBackground = true
    }
    .Start();

    while (b)
    {
        Thread.MemoryBarrier();

        //Console.WriteLine("b=" + b);
        //Thread.Sleep(100);
    }

    Console.WriteLine("b=" + b);
}
Отредактировано 17.11.2020 23:53 Aquilaware . Предыдущая версия .
Re[6]: Доступ к локальной переменной из разных потоков
От: pagid Россия  
Дата: 18.11.20 04:50
Оценка:
Здравствуйте, Muxa, Вы писали:

M>Запусти несколько раз и увидишь как меняется вывод программы от запуска к запуску.

Разве именно это не ожидаемое поведение?
Re[7]: Доступ к локальной переменной из разных потоков
От: Muxa  
Дата: 18.11.20 06:02
Оценка:
M>>Запусти несколько раз и увидишь как меняется вывод программы от запуска к запуску.
P>Разве именно это не ожидаемое поведение?

Не могу сказать без ТЗ к этой задаче.
Re[6]: Доступ к локальной переменной из разных потоков
От: IID Россия  
Дата: 18.11.20 07:48
Оценка:
Здравствуйте, karbofos42, Вы писали:

K>Проверяли у IEnumerable Count() и дальше работали с ним как-будто там точно есть такое количество элементов и оно не изменится в процессе работы.


ToCToU баги, мои любимые. Например в теликах LG так можно было неподписаннкю прошивку поставить.
kalsarikännit
Re[3]: Доступ к локальной переменной из разных потоков
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 18.11.20 10:56
Оценка:
Здравствуйте, Aquilaware, Вы писали:

A>Не все архитектуры CPU делают автоматическую синхронизацию кешей данных. Поэтому правильное решение состоит в указании ключевого слова volatile для переменной.


А какие не делают? Причём так, чтобы без явной синхронизации появления видимости какой-нибудь операции записи можно было бы ждать неопределённо долго?
The God is real, unless declared integer.
Re[5]: Доступ к локальной переменной из разных потоков
От: Mr.Delphist  
Дата: 19.11.20 08:38
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>Процессор не может атомарные операции переупорядочивать


То, что операция атомарна, вовсе не мешает процессору переложить две из них местами, если он не видит между ними зависимости. Запрет для instructions reordering может быть внесён, например, при помощи memory barrier. А уж атомарны или неатомарны операции — роли не играет.
Re[6]: Доступ к локальной переменной из разных потоков
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 19.11.20 09:00
Оценка:
Здравствуйте, Mr.Delphist, Вы писали:

MD>Здравствуйте, VladD2, Вы писали:


VD>>Процессор не может атомарные операции переупорядочивать


MD>То, что операция атомарна, вовсе не мешает процессору переложить две из них местами, если он не видит между ними зависимости. Запрет для instructions reordering может быть внесён, например, при помощи memory barrier. А уж атомарны или неатомарны операции — роли не играет.


Подозреваю, Влад мыслит в контексте x86 без вариантов, а у x86 очень жёсткий порядок, там

>> Reads are not reordered with other reads.

>> Writes are not reordered with older reads.
>> Writes to memory are not reordered with other writes, (немного незначащих исключений)
>> Locked instructions have a total order.

Остаётся только случай чтения после записи по другому адресу и оба не locked.

Ну или в C# атомарная всегда делается locked?
The God is real, unless declared integer.
Отредактировано 19.11.2020 10:28 netch80 . Предыдущая версия .
Re[6]: Доступ к локальной переменной из разных потоков
От: VladD2 Российская Империя www.nemerle.org
Дата: 19.11.20 12:10
Оценка:
Здравствуйте, Mr.Delphist, Вы писали:

MD>То, что операция атомарна, вовсе не мешает процессору переложить две из них местами, если он не видит между ними зависимости. Запрет для instructions reordering может быть внесён, например, при помощи memory barrier. А уж атомарны или неатомарны операции — роли не играет.


Порядок тут не причем просто потому, что тут нет зависимых переменных. Тут проблема может быть только в сбросе кэшей процессоров, которых может не быть.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[3]: Доступ к локальной переменной из разных потоков
От: Pavel Dvorkin Россия  
Дата: 19.11.20 12:56
Оценка: 1 (1) +1
Здравствуйте, Aquilaware, Вы писали:

A>Дотнет гарантирует атомарность всех примитивных типов у которых sizeof(T) <= IntPtr.Size. Это прописано в спецификации .NET Memory Model.


При условии, что операция атомарна с точки зрения процессора. Например, ++ не атомарна и Volatile не поможет. Поможет тут только Interlocked*
With best regards
Pavel Dvorkin
Re[7]: Доступ к локальной переменной из разных потоков
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 19.11.20 13:08
Оценка: 6 (1)
Здравствуйте, VladD2, Вы писали:

MD>>То, что операция атомарна, вовсе не мешает процессору переложить две из них местами, если он не видит между ними зависимости. Запрет для instructions reordering может быть внесён, например, при помощи memory barrier. А уж атомарны или неатомарны операции — роли не играет.


VD>Порядок тут не причем просто потому, что тут нет зависимых переменных.


Ты не сказал главное — являются ли атомарные операции синхронизированными в дотнете.
Судя по ссылкам типа такого, без volatile — не являются. С volatile они становятся acquire_release по спеке, чего достаточно для большинства случаев, и sequentially consistent по реальным реализациям, что обычно уже оверкилл

Для x86 это критично разве что для хитрых lock-free, для какого-нибудь ARM — уже и для обычных операций с эмуляцией защиты мьютексами.

VD>Тут проблема может быть только в сбросе кэшей процессоров, которых может не быть.


"Сброс" кэшей процессоров нафиг не нужен. Для завершения операции записи процессор демонстрирует владение данными этой строки операцией её перевода в единоличное владение с помощью соответствующей транзакции MESI-протокола (или близкого аналога)(*1); для чтения он должен через этот же протокол получить хотя бы одну копию строки и перевести её или в единоличное владение, или в разделяемое состояние. После этого, строка может оставаться в кэше неопределённо долго, и никто не заставляет её немедленно сбрасывать (хотя скорее всего это будет сделано после какого-то значительного переключения контекста).
Если уж ты начал рассказывать до такой глубины, то не порть существенные детали.
(*1) Особый случай общего store buffer на два hartʼа рассматриваем отдельно, но он даёт только более раннюю экспозицию результата.
The God is real, unless declared integer.
Re[8]: Доступ к локальной переменной из разных потоков
От: VladD2 Российская Империя www.nemerle.org
Дата: 19.11.20 13:26
Оценка:
Здравствуйте, netch80, Вы писали:

N>Ты не сказал главное — являются ли атомарные операции синхронизированными в дотнете.


Это уже другая проблема.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[4]: Доступ к локальной переменной из разных потоков
От: Sharov Россия  
Дата: 06.01.21 12:48
Оценка:
Здравствуйте, Pavel Dvorkin, Вы писали:

A>>Дотнет гарантирует атомарность всех примитивных типов у которых sizeof(T) <= IntPtr.Size. Это прописано в спецификации .NET Memory Model.

PD>При условии, что операция атомарна с точки зрения процессора. Например, ++ не атомарна и Volatile не поможет. Поможет тут только Interlocked*

Речь идет о чтении и записи.
Кодом людям нужно помогать!
Re[3]: Доступ к локальной переменной из разных потоков
От: Sharov Россия  
Дата: 07.01.21 13:34
Оценка:
Здравствуйте, Aquilaware, Вы писали:

A>Для x86 синхронизация между кешами данных происходит при: 1) Memory Barrier 2) автоматически каждые 10 нс


Я не совсем понимаю суть Memory Barrier -- это синхронизация кэшей для какой-то одной переменной,
или вообще всего содержимого?
Кодом людям нужно помогать!
Re[8]: Доступ к локальной переменной из разных потоков
От: Sharov Россия  
Дата: 07.01.21 13:48
Оценка:
Здравствуйте, netch80, Вы писали:

N>Ты не сказал главное — являются ли атомарные операции синхронизированными в дотнете.


По ссылке же написано, что Interlocked

The atomic_add_*, atomic_exchange_*, and atomic_cas_* IR opcodes all imply MONO_MEMORY_BARRIER_SEQ barriers (despite not explicitly being flagged) and behave as such in the IR with respect to reordering restrictions.


если слово "синхронизированными" относится к кэшам.
Кодом людям нужно помогать!
Re[4]: Доступ к локальной переменной из разных потоков
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 07.01.21 17:04
Оценка: 123 (2)
Здравствуйте, Sharov, Вы писали:

S>Здравствуйте, Aquilaware, Вы писали:


A>>Для x86 синхронизация между кешами данных происходит при: 1) Memory Barrier 2) автоматически каждые 10 нс


S>Я не совсем понимаю суть Memory Barrier -- это синхронизация кэшей для какой-то одной переменной,

S>или вообще всего содержимого?

Барьер памяти это вообще не про синхронизацию кэшей. Это про установление соотношений по времени между видимостью другими операций, сделанных в исполняемом треде, в его потоке процессорных инструкций, а также для действий в коде на языках более высокого уровня (начиная с C).

"Синхронизация кэшей" тут совершенно неуместный термин. Синхронизация видимости оперативной памяти (если описать более точно) это то, что производится другими средствами, по большей части прозрачно и неуправляемо для программы, которая только обращается к этому как клиент с операциями типа "читать память" и "писать память" (возможно, с модификаторами типа "попробовать обойти кэш"), и атомарными "прочитать-сравнить-записать", "прочитать-сложить-записать". А вот ближе к исполняемому ядру процессора...

Например, если вы пишете без дополнительных указаний

  int xvar, lock; // адрес выровнен как минимум у lock
  ...
  xvar = 1; // некоторая переменная, доступ к которой защищён спинлоком
  lock = 0; // освобождение спинлока (если не знаете, что это - считайте, что мьютекс)


компилятор имеет полное право переставить операции — и регулярно это делает по своему настроению в зависимости от тысячи факторов, которые предсказать невозможно. Если это не подходит, потому что нужен строгий порядок (как, например, между записью переменной под мьютексом и записью самого мьютекса для его освобождения) — нужен барьер компилятора. Для C/C++ этим является функция atomic_signal_fence, или asm("":::"memory") (GCC, Clang), или просто вызов посторонней функции, которую компилятор не знает в этот момент (кроме декларации) — вставить между этими двумя записями.

(А также может не только переставлять, а и группировать... например, если вы пишете
while (tmp = a) { // да-да, присвоение
  do_something_with(tmp);
}


компилятор может, увидев код для do_something_with, решить, что оно не меняет переменную `a`, и превратить в:

    if (tmp = a)
        for (;;)
            do_something_with(tmp);

Примеры из Linux memory barriers.

И аналогично, тут решается вставлением барьера компилятора. Альтернативно, чтение `a` помечается как volatile, но надо знать побочные эффекты этого метода. И, не отходя от кассы, volatile в C это не volatile в C#.)

Далее, с тем же примером: пусть у нас процессор с умением out-of-order исполнения и совсем расслабленной моделью отношений между командами. Имея в скомпилированном машинном коде что-то вроде (в синтаксисе x86)

  mov xvar, 1
  mov lock, 0


вы не можете быть уверенным, что процессор их не выполнит наоборот по каким-то своим произвольным соображениям — ну вот так свободные места во внутренней очереди команд разлеглись.
И если "mov lock, 0" выполнится раньше, чем "mov xvar, 1" — другие (ядра, процессоры, внешние устройства и т.п.) могут увидеть освобождение спинлока раньше, чем записано значение в xvar, захватить его самим и прочитать неверное. И тут это решается двумя вариантами:
1) или только "mov lock, 0" получает семантику release (для ARM, например, это STLR вместо STR). Этот барьер гарантирует, что все чтения и записи до "mov lock, 0" исполнятся раньше, чем эта запись (но ничего не говорит об операциях, которые в потоке команд находятся позже, неважно, чтения или записи — с ними никакой синхронизации тут не требуется, и они могут реально исполниться раньше, если процессору было так удобно).
2) или вставляется соответствующий release barrier отдельной командой. В таком случае гарантия распространяется на все записи после барьера (они будут выполнены позже всех чтений и записей до барьера), а не только на одну операцию (lock = 0).

"Записи исполнятся" означает, что внешний слой ОЗУ+кэши получит команды модификации данных по адресу, выполнит их и вернёт подтверждение (а как оно будет в чьём кэше — уже дело этого слоя). "Чтения исполнятся" — данные прочитаны и поступили в процессор (внутреннюю часть ближе, чем кэши).

На x86 (тут и дальше — x86-32 и x86-64 одинаково), любая запись (ну, почти — есть редкие исключения) обладает встроенным release-барьером, поэтому дополнительные флаги или команды не нужны; на ARM (если я правильно понял) запись без флагов упорядочена по отношению к предшествующим записям, но не чтениям (ради precise exceptions); но могут быть и такие места, где и предшествующая запись не упорядочена без соответствующих барьеров. (И все подобные барьеры включают в себя в обязательном неявном порядке и барьеры компилятора.)

Поэтому, для достижения такого эффекта, как нужно, надо писать:

C++:
int xvar;
std::atomic<int> lock;
...
xvar = 1;
lock.store(0, std::memory_order_release);


тут этот store() подразумевает и барьер компилятора, и барьер процессора перед записью в lock.

Или, с барьером отдельно от самой записи (чуть грубее),

int xvar;
int lock;
...
xvar = 1;
std::atomic_thread_fence(std::memory_order_release);
lock = 0;


C#:
int xvar;
int lock;
...
xvar = 1;
Volatile.Write(lock, 0);


то же самое про оба барьера.

И всё это делается само по себе безотносительно кэшей — которые сами по себе значительно усложняют саму работу с памятью, но на описанные мной вопросы прямо не влияют. Повторюсь, редставьте себе вместо системы кэшей просто некоторую сущность "интерфейс оперативной памяти", которая принимает/отправляет работает порциями по 64 байта (современные x86), и почему-то в зависимости от непредсказуемых факторов может выполнить операцию в 1 такт, а может в 300 — а теперь в этих условиях перечитайте всё вышеописанное. А после этого и тексты из MSDN типа:

> Reads the value of the specified field. On systems that require it, inserts a memory barrier that prevents the processor from reordering memory operations as follows: If a read or write appears after this method in the code, the processor cannot move it before this method.


> Writes the specified value to the specified field. On systems that require it, inserts a memory barrier that prevents the processor from reordering memory operations as follows: If a read or write appears before this method in the code, the processor cannot move it after this method.


станут понятны — вот как раз о выполнении команд тут и идёт речь. Для x86 дополнительные команды процессора не нужны: любая запись и так подразумевает подобное (хотя "before this method in the code" плохая формулировка, надо было сказать про операции, которые процессор исполняет для текущего треда), а чтение, наоборот, подразумевает, что все чтения и записи после данного в потоке процессорных инструкций будут выполнены после данной. И для него Volatile.{Read,Write} это только барьеры компилятора. Но уже для ARM надо явно помечать процессорные инструкции особыми флагами, а где-то может требоваться вводить более сильные меры.

(Примечание: некоторый объём тонкостей пропущен. Но для первичного понимания этого должно хватить с головой.)

Что имел в виду коллега Aquilaware:

A>>Для x86 синхронизация между кешами данных происходит при: 1) Memory Barrier 2) автоматически каждые 10 нс


это неверно в формулировке, как уже сказал — это не синхронизация между кэшами, это публикация всего сделанного внутри процессора-ещё-без-кэшей наружу тому слою, который реализует доступ к ОЗУ со всеми слоями кэшей перед ним. То есть, если цифры верны (не проверял), между завершением исполнения любой процессорной инструкции и тем, что сделанные ею записи в память будут опубликованы слою ОЗУ+кэши, пройдёт не более 10 нс. При необходимости можно это ускорить барьерами памяти (сюда для процессора входят как минимум sfence, mfence, также по отношению к большинству взаимодействий — in, out, xchg, lock+что-то, cpuid, переключения режимов). Впрочем, ускорение может быть только оттого, что последующие команды не будут мешаться тем, что до команды барьера.
The God is real, unless declared integer.
Отредактировано 08.01.2021 8:32 netch80 . Предыдущая версия .
Re[9]: Доступ к локальной переменной из разных потоков
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 07.01.21 17:23
Оценка:
Здравствуйте, Sharov, Вы писали:

N>>Ты не сказал главное — являются ли атомарные операции синхронизированными в дотнете.


S>По ссылке же написано, что Interlocked

S>

S>The atomic_add_*, atomic_exchange_*, and atomic_cas_* IR opcodes all imply MONO_MEMORY_BARRIER_SEQ barriers (despite not explicitly being flagged) and behave as such in the IR with respect to reordering restrictions.


S>если слово "синхронизированными" относится к кэшам.


Interlocked — это не синхронизированность, если само по себе.
Я рядом уже обширно написал
Автор: netch80
Дата: 07.01.21
, сначала прочитайте по ссылке.

Представим себе подряд операции:

int tmp1 = a;
int tmp2 = Interlocked.Exchange(b, 10);


даже двух операций достаточно для примера. Пусть это выливается в машинные инструкции типа:

  mov eax, a
  mov ebx, 10
  xchg b, ebx // в x86, xchg всегда атомарно
// теперь в eax - tmp1, в ebx - tmp2


Имеет ли право компилятор переставить их? Чтобы получилось:


  mov ebx, 10
  xchg b, ebx
  mov eax, a


Имеет ли право процессор переставить работу с "a" и "b" (поменять выполнения внутри себя соответствующих mov и xchg местами)? Должен ли компилятор вокруг xchg расставлять пометки процессору, чтобы не менять порядок действий?
Если есть защита от перестановки действий как компилятором, так и процессором — то это и есть их синхронизированность (которая ортогональна собственно атомарности), а если нет — то они не синхронизированы независимости от атомарности самого обмена в Interlocked.Exchange.

В случае x86, для xchg точно известно, что "Reads or writes cannot be reordered with I/O instructions, locked instructions, or serializing instructions" и к тому же " Locked instructions have a total order" (то есть все процессоры видят их в одном и том же порядке). А вот уже в ARM для CAS подобное не выполняется, если явно не поставить соответствующие флаги у инструкции.

Я подозреваю, что дотнет компилирует эти инструкции и 1) подразумевая барьер компилятора, и 2) ставя acquire-release вокруг exchange, на основании прочитанного и в этом треде, и вокруг. Но сам не проверял.
The God is real, unless declared integer.
Re[5]: Доступ к локальной переменной из разных потоков
От: Sharov Россия  
Дата: 12.01.21 10:32
Оценка:
Здравствуйте, netch80, Вы писали:

N>Барьер памяти это вообще не про синхронизацию кэшей. Это про установление соотношений по времени между видимостью другими операций, сделанных в исполняемом треде, в его потоке процессорных инструкций, а также для действий в коде на языках более высокого уровня (начиная с C).


А для ассемблера это разве не актуально, процессор все равно же может переставить инструкции (ooe)?

N>И аналогично, тут решается вставлением барьера компилятора. Альтернативно, чтение `a` помечается как volatile, но надо знать побочные эффекты этого метода. И, не отходя от кассы, volatile в C это не volatile в C#.)


А в чем отличия? Запрет на оптимизацию действий с переменной + барьер.
Кодом людям нужно помогать!
Re[6]: Доступ к локальной переменной из разных потоков
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 12.01.21 12:33
Оценка: 82 (2)
Здравствуйте, Sharov, Вы писали:

S>Здравствуйте, netch80, Вы писали:


N>>Барьер памяти это вообще не про синхронизацию кэшей. Это про установление соотношений по времени между видимостью другими операций, сделанных в исполняемом треде, в его потоке процессорных инструкций, а также для действий в коде на языках более высокого уровня (начиная с C).


S>А для ассемблера это разве не актуально, процессор все равно же может переставить инструкции (ooe)?


Для ассемблера — актуально, но с меньшим количеством случаев.
Во-первых, сам компилятор только преобразует текстовые формы инструкций в машинные коды и указания для линкера, но не может их переставить или сделать, как в примере с циклом, оптимизацию типа "я её прочитаю только один раз" — ему такое не положено.
Во-вторых, пределы перестановки действий процессора (или их видимых последствий) другие.
В том же примере,
  mov xvar, 1
  mov lock, 0


если это на x86 — то декларировано, что экспорт в этот самый storage системы "ОЗУ+кэши" с видимостью всем остальным процессорам — будет обеспечен так, что сначала все увидят запись 1 в xvar, а потом 0 в lock.
Вот как это реализовано — уже дело стораджа. Например, если память некэшируемая (область так помечена), это будут просто записи на шине. Если кэшируемая, но строка уже во владении этого процессора, будет обновление строки. Если строка shared, то вначале через шину синхронизации кэша будет получено единоличное владение. И так далее (читать гуглом по словам типа MESI, MOESI, etc., но не только набор состояний, а ещё и взаимодействия).

(А вот на многих других процессорах надо — повторюсь — пометить вторую запись признаком release или вставить между ними команду барьера.)

Вот на чтении интереснее. Формально порядок чтений на x86 строго тоже в порядке их выполнения. Но пусть написано

    mov eax, lock
    mov ebx, xvar


Формально чтение xvar обязано быть после чтения lock. Но, реально процессор может (и будет пытаться в случае кэшируемой памяти) "спекулятивно" прочитать xvar одновременно или быстрее, если чтение lock задерживается. Но-2: если он записал себе в буферный регистр значение xvar, а затем память по адресу lock кто-то поменял — он сбросит это прочтённое значение и отправит запрос записи ещё раз. Дальше опять же дело стораджа, что с этим делать (читать напрямую, потребовать себе копию строки, и так далее).

И вот тут на других архитектурах ещё чаще делают просто отсутствие такой зависимости — поэтому если после чтения lock не стоит барьер load-load (например, как acquire флаг на чтении lock), то процессору может запомнить устаревшее значение xvar.

N>>И аналогично, тут решается вставлением барьера компилятора. Альтернативно, чтение `a` помечается как volatile, но надо знать побочные эффекты этого метода. И, не отходя от кассы, volatile в C это не volatile в C#.)

S>А в чем отличия? Запрет на оптимизацию действий с переменной + барьер.

Нет, в C volatile не означает никакого барьера, даже барьера компилятора, по сравнению с остальными операциями (есть только общий программный порядок операций именно с volatile переменными). Этот признак говорит только то, что эту операцию нельзя объединять с любой другой, отменять, если компилятор считает ненужным, или, наоборот, вставлять чтение/запись по своему вкусу. При этом её можно переставлять с другими операциями, если это не запрещается компилятору чем-то другим (хотя не с другой volatile — вот порядок между ними должен соблюдаться... а все остальные могут их обтекать, как вода камни).

Например, пусть у нас инструкция сетевой карте послать пакет выглядит как 1) записать адрес пакета в хвост списка отправки и 2) обновить регистр сетевухи, который содержит адрес конца списка. В C это может быть что-то вроде:

volatile void **reg_send_list_begin, **reg_send_list_end; // адреса регистров сетевухи - содержат адреса
send_block *send_list_begin, *send_list_end;

...

// добавляем пакет в очередь
send_list_end->next = new_packet;
new_packet->prev = send_list_end;
send_list_end = new_packet;
// X
*reg_send_list_end = new_packet;


у вас нет гарантии, что последнюю операцию компилятор не поставит первой по своему настроению.
Если хотите такой гарантии — хотя бы вставьте atomic_signal_fence(memory_order_release) в позицию "// X" (точнее, нужно таки atomic_thread_fence, но я говорил про обеспечение для компилятора).

А вот Java, C# — volatile обязательно будет с такими барьерами (acquire на чтении, release на записи, оба — на комбинированной операции) и уровня уже thread (то есть и перестановки компилятора не допускаются, и барьеры будут выставлены для тех процессоров, где они нужны).
The God is real, unless declared integer.
Отредактировано 15.06.2023 8:42 netch80 . Предыдущая версия . Еще …
Отредактировано 12.01.2021 15:21 netch80 . Предыдущая версия .
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.