Доступ к локальной переменной из разных потоков
От: Shmj Ниоткуда  
Дата: 12.11.20 13:19
Оценка: 1 (1) -1
Есть локальная переменная bool-типа, которую изменяет один поток а читает другой. Какие могут быть проблемы?

Пример:

class Program
    {
        static void Main(string[] args)
        {
            bool b = true;

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

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

            Console.WriteLine("b=" + b);
        }
    }
Отредактировано 12.11.2020 19:48 Shmj . Предыдущая версия .
Re: Доступ к локальной переменной из разных потоков
От: vmpire Россия  
Дата: 12.11.20 14:11
Оценка: 1 (1) +5 -1
Здравствуйте, Shmj, Вы писали:

S>Есть локальная переменная bool-типа, которую изменяет один поток а читает другой. Какие могут быть проблемы?

Если переменная не volatile и нет лока вокруг, то чтение, вероятно, может быть соптимизировано и читающий поток не получит нового значения.
Отредактировано 12.11.2020 14:27 vmpire . Предыдущая версия .
Re: Доступ к локальной переменной из разных потоков
От: Pzz Россия https://github.com/alexpevzner
Дата: 12.11.20 14:17
Оценка: +5
Здравствуйте, Shmj, Вы писали:

S>Есть локальная переменная bool-типа, которую изменяет один поток а читает другой. Какие могут быть проблемы?


Большие.
Re: Доступ к локальной переменной из разных потоков
От: karbofos42 Россия  
Дата: 12.11.20 14:30
Оценка: +1
Здравствуйте, Shmj, Вы писали:

S>Есть локальная переменная bool-типа, которую изменяет один поток а читает другой. Какие могут быть проблемы?


Могут быть проблемы, если будет код типа:
if (a != boolValue) // boolValue - та самая общая переменная
{
  a = boolValue; // к этому моменту уже значение boolValue может быть изменено другим потоком
{

дальше можно еще нафантазировать.
А иногда можно взять CancellationToken и не делать свой велосипед на булевом флаге для отмены операции, если для этого такая переменная нужна.
Re: Доступ к локальной переменной из разных потоков
От: Muxa  
Дата: 12.11.20 17:12
Оценка: -2
S>Есть локальная переменная bool-типа, которую изменяет один поток а читает другой. Какие могут быть проблемы?

Проблем быть не должно.
У каждого потока будет свой экземпляр этой переменной.
Re[2]: Доступ к локальной переменной из разных потоков
От: Shmj Ниоткуда  
Дата: 12.11.20 19:49
Оценка:
Здравствуйте, vmpire, Вы писали:

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


Локальная не может быть volatile.

Добавил пример — попробуйте продемонстрировать случай, когда возникает проблема.
Re[2]: Доступ к локальной переменной из разных потоков
От: Shmj Ниоткуда  
Дата: 12.11.20 19:50
Оценка:
Здравствуйте, Pzz, Вы писали:

S>>Есть локальная переменная bool-типа, которую изменяет один поток а читает другой. Какие могут быть проблемы?


Pzz>Большие.


Добавил пример — продемонстрируйте проблему.
Re[2]: Доступ к локальной переменной из разных потоков
От: Shmj Ниоткуда  
Дата: 12.11.20 19:50
Оценка:
Здравствуйте, karbofos42, Вы писали:

K>Могут быть проблемы, если будет код типа:

K>
K>if (a != boolValue) // boolValue - та самая общая переменная
K>{
K>  a = boolValue; // к этому моменту уже значение boolValue может быть изменено другим потоком
K>{
K>

K>дальше можно еще нафантазировать.
K>А иногда можно взять CancellationToken и не делать свой велосипед на булевом флаге для отмены операции, если для этого такая переменная нужна.

Дык... По условиям — только один потом может изменить значение. Второй только читает.
Re[2]: Доступ к локальной переменной из разных потоков
От: Shmj Ниоткуда  
Дата: 12.11.20 19:51
Оценка:
Здравствуйте, Muxa, Вы писали:

M>Проблем быть не должно.

M>У каждого потока будет свой экземпляр этой переменной.

Если бы у каждого свой экземпляр — то второй поток не узнал бы, что первый изменил переменную. См. поясняющий пример.
Re[3]: Доступ к локальной переменной из разных потоков
От: vmpire Россия  
Дата: 12.11.20 20:05
Оценка: +6 -1
Здравствуйте, Shmj, Вы писали:

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

S>Локальная не может быть volatile.
S>Добавил пример — попробуйте продемонстрировать случай, когда возникает проблема.
В этом примере после компиляции переменная уже не будет локальной, это будет член класса, который сгенерируется из захваченной переменной.
В релизной компиляции всё будет зависеть от настроек компиляции и внутренней логики оптимизатора.
При компиляции наружного чтения переменной компилятор может и не догадаться, что переменная другого класса может меняться.
Пытаться демонстрировать случай я не буду, но я бы не стал рисковать, делая такую синхронизацию потоков.
Re[3]: Доступ к локальной переменной из разных потоков
От: karbofos42 Россия  
Дата: 12.11.20 20:08
Оценка: -1
Здравствуйте, Shmj, Вы писали:

S>Дык... По условиям — только один потом может изменить значение. Второй только читает.


Ну, второй и прочитал два раза и два раза получил разное значение.
К чему это приведёт — зависит от программы
Re[3]: Доступ к локальной переменной из разных потоков
От: karbofos42 Россия  
Дата: 12.11.20 20:15
Оценка:
Здравствуйте, Shmj, Вы писали:

S>Локальная не может быть volatile.


Осталось разобраться как она так шарится между потоками

S>Добавил пример — попробуйте продемонстрировать случай, когда возникает проблема.


Для этого нужно знать как в итоге собираются лямбды.
Рекомендую скопировать код, например, в https://sharplab.io
Отредактировано 17.11.2020 14:38 VladD2 . Предыдущая версия .
Re[3]: Доступ к локальной переменной из разных потоков
От: Muxa  
Дата: 12.11.20 20:21
Оценка: +1
M>>Проблем быть не должно.
M>>У каждого потока будет свой экземпляр этой переменной.
S>См. поясняющий пример.

Это не локальная переменная относительно функций потоков.
Re[3]: Доступ к локальной переменной из разных потоков
От: Pzz Россия https://github.com/alexpevzner
Дата: 12.11.20 20:23
Оценка: +1
Здравствуйте, Shmj, Вы писали:

S>Добавил пример — продемонстрируйте проблему.


Вплоть до переупорядочивания чтений/записей процессором (не компилятором, а именно процессором).

В твоем простом примере, конечно, такое вряд ли поймаешь — пока Sleep() отработает, все 100500 раз устаканится. Но в реальной жизни они будут, причем проявляться будут весьма неочевидным и невоспроизводимым образом.
Re[3]: Доступ к локальной переменной из разных потоков
От: Muxa  
Дата: 12.11.20 20:28
Оценка:
S>Добавил пример — попробуйте продемонстрировать случай, когда возникает проблема.

Например тут:
    public static int Main()
    {
        bool b = true;

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

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

        Console.WriteLine("b=" + b);
        return 0;
    }
Re[4]: Доступ к локальной переменной из разных потоков
От: Shmj Ниоткуда  
Дата: 12.11.20 20:50
Оценка:
Здравствуйте, Muxa, Вы писали:

M>Например тут:


И что за проблему вы увидели?
Re[5]: Доступ к локальной переменной из разных потоков
От: Muxa  
Дата: 13.11.20 03:41
Оценка:
M>>Например тут:

S>И что за проблему вы увидели?


Запусти несколько раз и увидишь как меняется вывод программы от запуска к запуску.
Re[4]: Доступ к локальной переменной из разных потоков
От: Mr.Delphist  
Дата: 13.11.20 07:56
Оценка:
Здравствуйте, Pzz, Вы писали:

Pzz>В твоем простом примере, конечно, такое вряд ли поймаешь — пока Sleep() отработает, все 100500 раз устаканится. Но в реальной жизни они будут, причем проявляться будут весьма неочевидным и невоспроизводимым образом.


На прошлой неделе аккурат это самое огрёб, из-за материализации linq-последовательности другим потоком, нежели тот который порождает

Упрощённо как-то так:
var b = init-bool-here();
var ohShit = data.Select(d => create-something-from-bool-val(b));
b = next-bool-logic();

Task.Run(() => foreach(var sh in ohShit) {...});


Когда оно так в три строчки нарисовано — всё сразу понятно, но в реальном коде это было куда более многословно. Когда увидел причину — поржал над собой
Отредактировано 17.11.2020 14:43 VladD2 . Предыдущая версия .
Re[4]: Доступ к локальной переменной из разных потоков
От: Shmj Ниоткуда  
Дата: 13.11.20 08:13
Оценка:
Здравствуйте, vmpire, Вы писали:

V>В этом примере после компиляции переменная уже не будет локальной, это будет член класса, который сгенерируется из захваченной переменной.

V>В релизной компиляции всё будет зависеть от настроек компиляции и внутренней логики оптимизатора.

Попробуйте вспомнить, откуда вы об этому узнали. Просто по слухам или где-то в авторитетных источниках прочитали?

Много раз слышал, но теперь начинаю сомневаться — может просто кто-то сказал и все за ним начали повторять.

В реальности ни разу не удалось продемонстрировать проблему.

V>При компиляции наружного чтения переменной компилятор может и не догадаться, что переменная другого класса может меняться.


И что? Создаст свою копию переменной, так что второй поток на нее влиять не будет?

V>Пытаться демонстрировать случай я не буду, но я бы не стал рисковать, делая такую синхронизацию потоков.


А причем тут синхронизация?
Re[4]: Доступ к локальной переменной из разных потоков
От: Shmj Ниоткуда  
Дата: 13.11.20 08:15
Оценка:
Здравствуйте, karbofos42, Вы писали:

K>Ну, второй и прочитал два раза и два раза получил разное значение.

K>К чему это приведёт — зависит от программы

Разное значение — т.к. второй поток изменил. Что странного?
Re[4]: Доступ к локальной переменной из разных потоков
От: Shmj Ниоткуда  
Дата: 13.11.20 08:15
Оценка:
Здравствуйте, karbofos42, Вы писали:

S>>Локальная не может быть volatile.

K>Осталось разобраться как она так шарится между потоками

И как? В чем может быть проблема?
Re[4]: Доступ к локальной переменной из разных потоков
От: Shmj Ниоткуда  
Дата: 13.11.20 08:16
Оценка:
Здравствуйте, Muxa, Вы писали:

M>Это не локальная переменная относительно функций потоков.


Хорошо. Какую проблему вы видите и какой вариант решения?
Re[4]: Доступ к локальной переменной из разных потоков
От: Shmj Ниоткуда  
Дата: 13.11.20 08:42
Оценка:
Здравствуйте, Pzz, Вы писали:

Pzz>Вплоть до переупорядочивания чтений/записей процессором (не компилятором, а именно процессором).


Был такой пример:


        static void Main(string[] args)
        {
            bool a;

            Thread t = new Thread(() =>
            {
                a = true;
                while (a)
                {
                }
            });
            t.Start();

            Thread.Sleep(1000);
            a = false;

            t.Join();
        }


Прога зависает... Но тут два потока и пишут и читают — если убрать из второго запись — то все ОК.
Re[5]: Доступ к локальной переменной из разных потоков
От: karbofos42 Россия  
Дата: 13.11.20 08:52
Оценка:
Здравствуйте, Shmj, Вы писали:

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


K>>Ну, второй и прочитал два раза и два раза получил разное значение.

K>>К чему это приведёт — зависит от программы

S>Разное значение — т.к. второй поток изменил. Что странного?


Ничего странного. Был вопрос к чему это может привести.
Я напомнил базовую вещь. Если читающий код будет правильно с этой изменяемой переменной работать, то ничего не будет.
У меня так коллеги из-за LINQ удивлялись по незнанию.
Проверяли у IEnumerable Count() и дальше работали с ним как-будто там точно есть такое количество элементов и оно не изменится в процессе работы.
Теперь всегда ToList() пишут и "фиксируют" коллекцию
Re[5]: Доступ к локальной переменной из разных потоков
От: karbofos42 Россия  
Дата: 13.11.20 08:56
Оценка: 12 (1) +1
Здравствуйте, Shmj, Вы писали:

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


S>>>Локальная не может быть volatile.

K>>Осталось разобраться как она так шарится между потоками

S>И как? В чем может быть проблема?


Я для кого про sharplab писал?
Я вот даже не поленился и скопировал пример, получился такой код:
internal class Program
{
    [CompilerGenerated]
    private sealed class <>c__DisplayClass0_0
    {
        public bool b;

        internal void <Main>b__0()
        {
            Thread.Sleep(5000);
            b = false;
        }
    }

    private static void Main(string[] args)
    {
        <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
        <>c__DisplayClass0_.b = true;
        Thread thread = new Thread(new ThreadStart(<>c__DisplayClass0_.<Main>b__0));
        thread.IsBackground = true;
        thread.Start();
        while (<>c__DisplayClass0_.b)
        {
            Console.WriteLine("b=" + <>c__DisplayClass0_.b);
            Thread.Sleep(1000);
        }
        Console.WriteLine("b=" + <>c__DisplayClass0_.b);
    }
}


Вопросы про "локальную" переменную и как она в лямбду попадает остались?
Re[3]: Доступ к локальной переменной из разных потоков
От: Jack128  
Дата: 13.11.20 08:57
Оценка:
Здравствуйте, Shmj, Вы писали:

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


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


S>Локальная не может быть volatile.


https://docs.microsoft.com/en-us/dotnet/api/system.threading.volatile.read?view=net-5.0
Re[5]: Доступ к локальной переменной из разных потоков
От: vmpire Россия  
Дата: 13.11.20 12:07
Оценка:
Здравствуйте, Shmj, Вы писали:


V>>В этом примере после компиляции переменная уже не будет локальной, это будет член класса, который сгенерируется из захваченной переменной.

V>>В релизной компиляции всё будет зависеть от настроек компиляции и внутренней логики оптимизатора.
S>Попробуйте вспомнить, откуда вы об этому узнали. Просто по слухам или где-то в авторитетных источниках прочитали?
Где читал — не помню. Но решарпер это подтверждает.

V>>При компиляции наружного чтения переменной компилятор может и не догадаться, что переменная другого класса может меняться.

S>И что? Создаст свою копию переменной, так что второй поток на нее влиять не будет?
Нет, просто при компиляции в нативный код не сгенерирует инструкцию чтения ячейки памяти. Вынесет чтение за цикл, так как в цикле она не меняется.
Вот тут хорошо расписано про переупорядочивание.


V>>Пытаться демонстрировать случай я не буду, но я бы не стал рисковать, делая такую синхронизацию потоков.

S>А причем тут синхронизация?
При том, что у вас два зависящих потока, синхронизирующиеся через булевскую переменную.
Re: Доступ к локальной переменной из разных потоков
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 16.11.20 05:24
Оценка: +1
Здравствуйте, Shmj, Вы писали:

S>Есть локальная переменная bool-типа, которую изменяет один поток а читает другой. Какие могут быть проблемы?


А есть программа, которая пишет "Hello world". Какие могут быть проблемы?

S>Пример:


Пример ни о чём.

Зачем одна нить пишет эту переменную?
Зачем другая нить её читает?

Ответь на эти вопросы, тогда будет о чём говорить.
The God is real, unless declared integer.
Re: Доступ к локальной переменной из разных потоков
От: xma  
Дата: 16.11.20 05:38
Оценка:
Здравствуйте, Shmj, Вы писали:

S>Есть локальная переменная bool-типа, которую изменяет один поток а читает другой. Какие могут быть проблемы?


делай сразу как положено и не отсвечивай как ты вообще .NET кодером робишь ?

Другим подходом является использование ключевого слова volatile, которым помечаются необходимые поля класса. Оно заставляет компилятор генерировать барьеры памяти при каждом чтении и записи в переменную, помеченной volatile. Данный подход хорош в том случае, когда у вас один поток, или одни потоки только читают, а другие только записывают. Если же вам необходимо читать и изменять в одном потоке, то стоит воспользоваться оператором lock.


Основы многопоточности в .NET Framework
https://habr.com/ru/company/nix/blog/260745/
Re: Доступ к локальной переменной из разных потоков
От: VladD2 Российская Империя www.nemerle.org
Дата: 17.11.20 14:28
Оценка:
Здравствуйте, Shmj, Вы писали:

S>Есть локальная переменная bool-типа, которую изменяет один поток а читает другой. Какие могут быть проблемы?


В принципе это будет работать. Дотнет не гарантирует атомарности и корректного разделение переменных и полей, но поле типа bool и целые, на практике, работают атомарно и корректно. Если ты используешь переменную просто для завершения потока — это прокатит и проблем не будет.

Надо еще понимать, что в данном примере создается класс замыкания и переменная "b" становится его полем.

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

С другой стороны есть и явное преимущество — нет блокировки и мемори-барьеров, сбрасывающих кэш. Они все равно случатся рано или поздно, но лучше поздно, так как это не повлияет на производительность.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[2]: Доступ к локальной переменной из разных потоков
От: VladD2 Российская Империя www.nemerle.org
Дата: 17.11.20 14:30
Оценка:
Здравствуйте, vmpire, Вы писали:

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


На практике это не так и MS поддерживает это негласное соглашение, так как в ином случае куча программ перестанет работать. Это вам не плюсы.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[4]: Доступ к локальной переменной из разных потоков
От: VladD2 Российская Империя www.nemerle.org
Дата: 17.11.20 14:37
Оценка:
Здравствуйте, vmpire, Вы писали:

V>В этом примере после компиляции переменная уже не будет локальной, это будет член класса, который сгенерируется из захваченной переменной.


Да.

V>В релизной компиляции всё будет зависеть от настроек компиляции и внутренней логики оптимизатора.


Нет. Если перемеменная уходит в замыкание, то она уходит в замыкание. Не может другой поток без замыкания к ней доступ получить, так что в этом примере все убдет работать.

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

V>При компиляции наружного чтения переменной компилятор может и не догадаться, что переменная другого класса может меняться.


Компилятор не человек. Он догадайки не играет. У него набор оптимизаций (очень скудный для шарпа и чуть более расширенный для ИЛа). Если объект попадает в замыкание, то единсвенное что может статься — это замыкание может быть занлайнено. Но:
1. МС не умеет на сегодня инлайнить замыкания (вообще).
2. Если замыкание уходит в функцию которую нельзя инлайнить, а функция потока именно такая, то инлайнинг невозможен.
3. В МС знают о вольном использовании переменных между потоками и поддерживают этот инваринт между версиями фрэмворка.

Так что не фиг пугать людей. Работало, работает и работать будет.

V>Пытаться демонстрировать случай я не буду, но я бы не стал рисковать, делая такую синхронизацию потоков.




https://www.youtube.com/watch?v=j978pwMZjmE
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[4]: Доступ к локальной переменной из разных потоков
От: VladD2 Российская Империя www.nemerle.org
Дата: 17.11.20 14:40
Оценка:
Здравствуйте, Pzz, Вы писали:

Pzz>Вплоть до переупорядочивания чтений/записей процессором (не компилятором, а именно процессором).


А где здесь вообще есть порядок?

Pzz>В твоем простом примере, конечно, такое вряд ли поймаешь — пока Sleep() отработает, все 100500 раз устаканится. Но в реальной жизни они будут, причем проявляться будут весьма неочевидным и невоспроизводимым образом.


Вот именно в его простом примере проблем нет! А в более сложном нужно смотреть отдельно.

Ну и не выдумывай. Процессор не может атомарные операции переупорядочивать. А чтение и запись була и интов в дотнете обеспечиваются атомарными.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[4]: Доступ к локальной переменной из разных потоков
От: VladD2 Российская Империя www.nemerle.org
Дата: 17.11.20 14:42
Оценка:
Здравствуйте, Muxa, Вы писали:

M>Например тут:

И чё? Выйдет точно так же.

Ты же не полагаешься на точное время работы потока. А если важно именно время, то это не верный подход.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[5]: Доступ к локальной переменной из разных потоков
От: VladD2 Российская Империя www.nemerle.org
Дата: 17.11.20 14:45
Оценка:
Здравствуйте, Mr.Delphist, Вы писали:

MD>На прошлой неделе аккурат это самое огрёб, из-за материализации linq-последовательности другим потоком, нежели тот который порождает


Совсем не то, но так то да — и там, и там потоки.

Ну, надо же соображать, что делаешь? Одно дело положиться на булеву переменную, которая читается и пишется атомарно, а другое дело в другой поток ленивый объект передать, заполнение которого основано на не статических данных.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[5]: Доступ к локальной переменной из разных потоков
От: VladD2 Российская Империя www.nemerle.org
Дата: 17.11.20 14:48
Оценка:
Здравствуйте, Shmj, Вы писали:

S>Много раз слышал, но теперь начинаю сомневаться — может просто кто-то сказал и все за ним начали повторять.


Да просто фобии и слепое копирование. Сто раз видел подобное в других областях. Куча идиотов реализуют Dispose-паттерн для объектов никогда не хранящих неуправляемых ресурсов и т.п. Просто делают все под копирку не осмысляя происходящего.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[5]: Доступ к локальной переменной из разных потоков
От: VladD2 Российская Империя www.nemerle.org
Дата: 17.11.20 14:50
Оценка: +1
Здравствуйте, Shmj, Вы писали:

S>Разное значение — т.к. второй поток изменил. Что странного?


Ну, типа если ты ожидал, что всегда будет одно и то же число строк на экран выведено, то тебя это может удивить. Но если тебе нужно закончить поток после того как, скажем, пользователь нажал на кнопочку — это вполне нормальное решение. Тебе не трогает, что поток может поработать еще несколько миллисекунд. Тебе важен сам факт его окончания в приемлемое время. И это произойдет.

Но надо понимать, что все это работает на простых типах. Для сложных могут начаться проблемы атомарности и т.п. По этому в сложных случаях нужно как следует думать.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[2]: Доступ к локальной переменной из разных потоков
От: VladD2 Российская Империя www.nemerle.org
Дата: 17.11.20 14:54
Оценка:
Здравствуйте, netch80, Вы писали:

N>Пример ни о чём.


Нормальный пример. Вместо вывода на консоль можно влепить любой долго работающий код.

N>Зачем одна нить пишет эту переменную?

N>Зачем другая нить её читает?

Чтобы прервать выполнение потока, ваш КО

Скажем в одном потоке ведутся расчеты, а в гуевом юзер нажал на кнопку "Прервать".
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[3]: Доступ к локальной переменной из разных потоков
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 17.11.20 15:26
Оценка:
Здравствуйте, VladD2, Вы писали:

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

N>>Пример ни о чём.
VD>Нормальный пример. Вместо вывода на консоль можно влепить любой долго работающий код.

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

N>>Зачем одна нить пишет эту переменную?

N>>Зачем другая нить её читает?
VD>Чтобы прервать выполнение потока, ваш КО

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

VD>Скажем в одном потоке ведутся расчеты, а в гуевом юзер нажал на кнопку "Прервать".


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

UPD: Ну меня поняли, надеюсь. Тут нехорошо идти в обе стороны — и циклиться на экстремальных ситуациях, и считать, что всё всегда будет идеально. Пример ТС слишком узок, твои комментарии по деталям уместны, но не дают общей системы. А ТС явно нужно учить общее понимание проблемы, чтобы не писать дурные провокационные вопросы.
The God is real, unless declared integer.
Отредактировано 17.11.2020 17:47 netch80 . Предыдущая версия .
Re[6]: Доступ к локальной переменной из разных потоков
От: Mr.Delphist  
Дата: 17.11.20 18:04
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>Ну, надо же соображать, что делаешь? Одно дело положиться на булеву переменную, которая читается и пишется атомарно, а другое дело в другой поток ленивый объект передать, заполнение которого основано на не статических данных.


Так это ж классика — сначала асинхронности не было и в помине, код был сугубо линейным. Затем добавили ещё чуть чуть, затем через время ещё чуть-чуть — и вот оно. И само собой, это вовсе не три строчки, мяса там куда больше.
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...
Пока на собственное сообщение не было ответов, его можно удалить.