Блокировка в С#

Я все еще немного неясен, и когда нужно обернуть замок вокруг некоторого кода. Мое общее правило состоит в том, чтобы обернуть операцию в блокировку, когда она читает или записывает статическую переменную. Но когда статическая переменная ТОЛЬКО считывается (например, она только для чтения, которая задается при инициализации типа), доступ к ней не обязательно должен быть завернут в оператор блокировки, правильно? Недавно я увидел некоторый код, похожий на следующий пример, и это заставило меня подумать, что в моем многопоточном знании могут быть некоторые пробелы:

class Foo
{
    private static readonly string bar = "O_o";

    private bool TrySomething()
    {
        string bar;

        lock(Foo.objectToLockOn)
        {
            bar = Foo.bar;          
        }       

        // Do something with bar
    }
}

Это просто не имеет смысла для меня - почему бы там возникнуть проблема concurrency с READING регистром?

Кроме того, этот пример вызывает еще один вопрос. Один из них лучше, чем другой? (Например, пример два удерживает блокировку на меньшее время?) Полагаю, я мог бы разобрать MSIL...

class Foo
{
    private static string joke = "yo momma";

    private string GetJoke()
    {
        lock(Foo.objectToLockOn)
        {
            return Foo.joke;
        }
    }
}

против.

class Foo
{
    private static string joke = "yo momma";

        private string GetJoke()
        {
            string joke;

            lock(Foo.objectToLockOn)
            {
                joke = Foo.joke;
            }

            return joke;
        }
}

Ответы

Ответ 1

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

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

class Foo
{
    private int count = 0;
    public void TrySomething()    
    {
        count++;
    }
}

Вы можете предположить, что два потока, выполняющие метод TrySomething, будут в порядке. Но его нет.

  • Thread A считывает значение count (0) в регистр, чтобы его можно было увеличить.
  • Контекстный переключатель! Планировщик потоков решает, что поток A имеет достаточно времени выполнения. Далее в строке находится Thread B.
  • Thread B считывает значение count (0) в регистр.
  • Thread B увеличивает регистр.
  • Thread B сохраняет результат (1) для подсчета.
  • Переключение контекста обратно в A.
  • Thread A перезагружает регистр со значением count (0), сохраненным в его стеке.
  • Thread A увеличивает регистр.
  • Thread A сохраняет результат (1) для подсчета.

Итак, хотя мы дважды вызывали count ++, значение count только что изменилось с 0 на 1. Давайте сделаем код потокобезопасным:

class Foo
{
    private int count = 0;
    private readonly object sync = new object();
    public void TrySomething()    
    {
        lock(sync)
            count++;
    }
}

Теперь, когда Thread A прерывается, Thread B не может испортить счет, потому что он попадет в оператор блокировки, а затем заблокирует до тех пор, пока Thread A не выпустит синхронизацию.

Кстати, есть альтернативный способ сделать инкрементные Int32s и Int64s потокобезопасными:

class Foo
{
    private int count = 0;
    public void TrySomething()    
    {
        System.Threading.Interlocked.Increment(ref count);
    }
}

Что касается второй части вашего вопроса, я думаю, что я просто пойду с тем, что легче читать, любая разница в производительности будет незначительной. Ранняя оптимизация - это корень всего зла и т.д.

Почему многопоточность жесткая

Ответ 2

Чтение или запись 32-разрядного или меньшего поля - это атомная операция в С#. Насколько мне известно, нет необходимости в блокировке кода, который вы представили.

Ответ 3

Мне кажется, что в вашем первом случае блокировка не нужна. Использование статического инициализатора для инициализации бара гарантируется потокобезопасностью. Поскольку вы только когда-либо читали значение, нет необходимости блокировать его. Если значение никогда не изменится, никогда не будет споров, зачем вообще блокировать?

Ответ 4

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

Тем не менее, я пришел из Земли Java, где все чтения и записи переменных являются атомарными действиями. Другие ответы здесь показывают, что .NET отличается.

Ответ 5

Грязный читает?

Ответ 6

На мой взгляд, вы должны очень стараться не ставить статические переменные в положение, где их нужно читать/записывать из разных потоков. В этом случае они являются глобальными переменными free-for-all, а глобальные значения почти всегда являются плохими.

При этом, если вы ставите статическую переменную в такой позиции, вы можете заблокировать ее во время чтения, на всякий случай - помните, что другой поток, возможно, набросился и изменил значение во время чтения, и если это возможно, вы можете получить поврежденные данные. Чтения не обязательно являются атомными операциями, если вы не уверены, что они заблокированы. То же самое с write - они не всегда являются атомарными операциями.

Изменить: Как отметил Марк, для некоторых примитивов в чтениях С# всегда атомарно. Но будьте осторожны с другими типами данных.

Ответ 7

Что касается вашего вопроса "что лучше", то они одинаковы, поскольку область функций не используется ни для чего другого.