Блокировка в С#
Я все еще немного неясен, и когда нужно обернуть замок вокруг некоторого кода. Мое общее правило состоит в том, чтобы обернуть операцию в блокировку, когда она читает или записывает статическую переменную. Но когда статическая переменная ТОЛЬКО считывается (например, она только для чтения, которая задается при инициализации типа), доступ к ней не обязательно должен быть завернут в оператор блокировки, правильно? Недавно я увидел некоторый код, похожий на следующий пример, и это заставило меня подумать, что в моем многопоточном знании могут быть некоторые пробелы:
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
Что касается вашего вопроса "что лучше", то они одинаковы, поскольку область функций не используется ни для чего другого.