Подтвердить что ты не робот

Блокировка памяти по оператору блокировки

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

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

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;
}
  • Метод Update; это оператор _usingMethod1 = true, который всегда выполняется перед получением или установкой свойства? или из-за проблемы переупорядочения мы не можем гарантировать это?

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

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

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

Ответ 1

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

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

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

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

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

Тем не менее, все это тоо, если мы имеем дело только с одним потоком. Заказ, так как он воспринимается этой нитью, всегда сохраняется. Фактически, без этой основной гарантии никакая программа никогда не будет работать правильно. Реальная проблема заключается в том, как другие потоки воспринимают чтение и запись. Вот где вам нужно беспокоиться.

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

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

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

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

Ответ 2

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

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

private object locker = new object();

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

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