Блокировка в C # - PullRequest
       37

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

14 голосов
/ 20 сентября 2008

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

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;
        }
}

Ответы [ 7 ]

23 голосов
/ 20 сентября 2008

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

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

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 не может связываться со счетчиком, потому что он попадет в оператор блокировки и затем будет блокироваться, пока поток A не освободит синхронизацию.

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

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

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

Почему многопоточность трудна

7 голосов
/ 20 сентября 2008

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

3 голосов
/ 20 сентября 2008

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

1 голос
/ 20 сентября 2008

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

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

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

1 голос
/ 20 сентября 2008

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

1 голос
/ 20 сентября 2008

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

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

0 голосов
/ 20 сентября 2008

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

...