Вопрос

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

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

    private bool TrySomething()
    {
        string bar;

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

        // Do something with bar
    }
}

Это просто не имеет смысла для меня - почему из-за проблем с параллелизмом при ЧТЕНИИ регистра?

Кроме того, в этом примере возникает еще один вопрос.Является ли одно из них лучше другого?(Например.второй пример удерживает блокировку в течение меньшего времени?) Полагаю, я мог бы разобрать 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;
        }
}
Это было полезно?

Решение

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

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

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

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

  1. Поток A считывает значение count (0) в регистр, чтобы его можно было увеличить.
  2. Переключение контекста!Планировщик потоков решает, что у потока A было достаточно времени выполнения.Следующий на очереди - поток B.
  3. Поток B считывает значение count (0) в регистр.
  4. Поток B увеличивает регистр.
  5. Поток B сохраняет результат (1) для подсчета.
  6. Переключение контекста обратно на A.
  7. Поток A перезагружает регистр со значением count (0), сохраненным в его стеке.
  8. Поток A увеличивает регистр.
  9. Поток A сохраняет результат (1) для подсчета.

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

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

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

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

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

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

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

Другие советы

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

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

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top