Барьер памяти с помощью оператора блокировки

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

Рассмотрим следующий сценарий:

private object _object1 = null;    
private object _object2 = null;
private bool _usingObject1 = false;

private object MyObject
{
    get 
    {
        if (_usingObject1)
        {
            return _object1;
        }
        else
        {
            return _object2;
        }
    }
    set 
    {
        if (_usingObject1)
        {
           _object1 = value;
        }
        else
        {
           _object2 = value;
        }
    }
}

private void Update()
{
    _usingMethod1 = true;
    SomeProperty = FooMethod();
    //..
    _usingMethod1 = false;
}
  1. Метод Update; всегда ли выполняется оператор _usingMethod1 = true перед получением или установкой свойства? или из-за проблем с повторным заказом мы не можем этого гарантировать?

  2. Должны ли мы использовать volatile как

    private volatile bool _usingMethod1 = false;
    
  3. Если мы используем lock;, можем ли мы гарантировать, что каждый оператор в блокировке будет выполняться в следующем порядке:

    private void FooMethod()
    {
        object locker = new object();
        lock (locker)
        {
            x = 1;
            y = a;
            i++;
        }
    }
    

person Jalal Said    schedule 16.05.2010    source источник


Ответы (2)


Тема барьеров памяти довольно сложна. Время от времени это даже сбивает с толку экспертов. Когда мы говорим о барьере памяти, мы на самом деле объединяем две разные идеи.

  • Ограждение захвата: барьер памяти, в котором другим операциям чтения и записи не разрешено перемещаться перед ограждением.
  • Ограждение освобождения: барьер памяти, в котором другие операции чтения и записи не могут перемещаться после ограждения.

Барьер памяти, который создает только одно из двух, иногда называют полузабором. Барьер памяти, который создает и то, и другое, иногда называют полным барьером.

Ключевое слово volatile создает полузаборы. Чтение изменчивых полей имеет семантику приобретения, тогда как запись имеет семантику освобождения. Это означает, что никакая инструкция не может быть перемещена перед чтением или после записи.

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

Однако все это спорно, если нас интересует только один поток. Порядок, как он воспринимается этим потоком, всегда сохраняется. На самом деле, без этой фундаментальной гарантии ни одна программа никогда не работала бы правильно. Настоящая проблема заключается в том, как другие потоки воспринимают операции чтения и записи. Вот где вам нужно беспокоиться.

Итак, чтобы ответить на ваши вопросы:

  1. С точки зрения одного потока... да. С точки зрения другого треда... нет.

  2. По-разному. Это может сработать, но мне нужно лучше понять, чего вы пытаетесь достичь.

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

person Brian Gideon    schedule 16.05.2010
comment
Спасибо за информацию, это действительно помогает мне лучше понять концепцию. Мне нужно добиться того, чтобы инструкция _usingMethod1 = true всегда выполнялась перед следующей инструкцией SomeProperty = FooMethod(); Как это сделать в многопоточном сценарии? по: _usingMethod1 = true; Thread.MemoryBarrier(); SomeProperty = FooMethod(); или заблокировать для полных заборов, чтобы не было повторного заказа: lock (locker) { _usingMethod1= true; } SomeProperty = FooMethod(); или, может быть, просто сделав _usingMethod1 изменчивой переменной. Спасибо за вашу помощь. - person Jalal Said; 17.05.2010
comment
Я бы обернул все содержимое метода Update в замок. В дополнение к барьерам памяти это также гарантирует атомарность, что не менее важно. Кроме того, эти идиомы без блокировки (через volatile, Thread.MemoryBarrier и т. д.) невероятно сложно понять правильно. - person Brian Gideon; 17.05.2010
comment
Ваши определения захвата и освобождения забора не совсем верны. Acquire также предотвращает перемещение предыдущих операций чтения после ограничения, а выпуск предотвращает перемещение последующих операций записи перед ним. В противном случае вы могли бы переместить все до приобретения или после выпуска через забор, и забор вообще не давал бы никаких гарантий. - person relatively_random; 21.10.2020
comment
Еще одна тонкость - volatile не гарантирует создание фактических заборов захвата/выпуска. Нестабильное чтение гарантирует, что все последующие обращения к памяти не будут переупорядочены до этого чтения, но любое предыдущее чтение других переменных может перемещаться ниже непостоянного чтения. Ограждение захвата также предотвратило бы перемещение всех предыдущих операций чтения ниже любого последующего доступа к памяти. То же самое и с изменчивой записью — это не ограничение релиза, а просто запись релиза. - person relatively_random; 21.10.2020
comment
Наконец, насколько я знаю, блокировки требуют только получения semantics при входе и освобождения semantics при выходе, а не захвата/освобождения даже заборов. Другими словами, им просто нужно предотвратить побег операций внутри блока, но им не нужно влиять ни на что за его пределами. Теоретически несвязанный доступ к памяти, произошедший до блока блокировки, может быть переупорядочен после него или наоборот. - person relatively_random; 21.10.2020

Ключевое слово volatile здесь ничего не делает. У него очень слабые гарантии, он не подразумевает барьер памяти. Ваш код не показывает создание другого потока, поэтому трудно догадаться, требуется ли блокировка. Однако это жесткое требование, если два потока могут одновременно выполнять Update() и использовать один и тот же объект.

Имейте в виду, что опубликованный вами код блокировки ничего не блокирует. Каждый поток будет иметь свой собственный экземпляр объекта "locker". Вы должны сделать его частным полем вашего класса, созданным конструктором или инициализатором. Таким образом:

private object locker = new object();

private void Update()
{
    lock (locker)
    {
        _usingMethod1 = true;
        SomeProperty = FooMethod();
        //..
        _usingMethod1 = false;
    }
}

Обратите внимание, что в задании SomeProperty также будет гонка.

person Hans Passant    schedule 16.05.2010
comment
У volatile есть барьер памяти; и поэтому я спросил, будет ли этот барьер памяти с ним подавлять переупорядочение, поэтому _usingMethod1 = true всегда будет гарантировать выполнение перед получением или установкой свойства SomeProperty, я предоставляю блокировку только для барьера памяти, а не для проблема синхронизации с другими потоками, и поэтому я сделайте это локальной переменной внутри метода специально, потому что я спрашивал, позволит ли это избежать переупорядочения инструкций внутри блокировки. - person Jalal Said; 17.05.2010
comment
Отдельный поток всегда имеет согласованное представление переменных, которые он использует. Программы не могли бы работать, если бы это было не так. - person Hans Passant; 17.05.2010