Дважды проверенная блокировка в .NET
-
23-08-2019 - |
Вопрос
я наткнулся на это статья обсуждаем, почему парадигма блокировки с двойной проверкой не работает в Java.Верна ли парадигма для .NET (в частности, C#), если объявлены переменные volatile
?
Решение
Реализация шаблона Singleton в C# об этой проблеме рассказывается в третьей версии.
Там говорится:
Если сделать переменную экземпляра изменчивой, это может заставить ее работать, как и явные вызовы барьеров памяти, хотя в последнем случае даже эксперты не могут прийти к единому мнению, какие именно барьеры необходимы.Я стараюсь избегать ситуаций, когда эксперты не приходят к единому мнению, что правильно, а что нет!
Автор, похоже, подразумевает, что двойная блокировка сработает с меньшей вероятностью, чем другие стратегии, и поэтому ее не следует использовать.
Другие советы
Блокировка с двойной проверкой теперь работает как в Java, так и в C# (модель памяти Java изменилась, и это один из последствий).Однако вы должны это получить точно верно.Если вы хоть немного что-то испортите, вы вполне можете потерять потокобезопасность.
Как уже говорилось в других ответах, если вы реализуете одноэлементный шаблон есть гораздо лучшие способы сделать это.Лично, если бы я оказался в ситуации, когда мне пришлось выбирать между блокировкой с двойной проверкой и кодом «блокировать каждый раз», я бы использовал блокировку каждый раз, пока не получу реальных доказательств того, что это вызывает узкое место.Когда дело доходит до заправки ниток, простая и очевидно правильная выкройка многого стоит.
В .NET 4.0 появился новый тип: Lazy<T>
это избавит вас от беспокойства по поводу неправильного шаблона.Это часть новой библиотеки параллельных задач.
См. Центр разработки параллельных вычислений MSDN: http://msdn.microsoft.com/en-us/concurrency/default.aspx
Кстати, доступен бэкпорт (я считаю, что он не поддерживается) для .NET 3.5 SP1. здесь.
Обратите внимание, что в Java (и, скорее всего, в .Net) блокировка с двойной проверкой для одноэлементной инициализации совершенно не нужна и не работает.Поскольку классы не инициализируются до тех пор, пока они не будут впервые использованы, желаемая ленивая инициализация уже достигается этим;
private static Singleton instance = new Singleton();
Если ваш класс Singleton не содержит таких вещей, как константы, к которым можно получить доступ до первого использования экземпляра Singleton, это все, что вам нужно сделать.
Я не понимаю, почему все люди говорят, что блокировка с двойной проверкой — это плохой шаблон, но не адаптируют код, чтобы он работал правильно.На мой взгляд, приведенный ниже код должен работать нормально.
Если кто-нибудь может сказать мне, страдает ли этот код от проблемы, упомянутой в статье Кэмерона, пожалуйста, сделайте это.
public sealed class Singleton {
static Singleton instance = null;
static readonly object padlock = new object();
Singleton() {
}
public static Singleton Instance {
get {
if (instance != null) {
return instance;
}
lock (padlock) {
if (instance != null) {
return instance;
}
tempInstance = new Singleton();
// initialize the object with data
instance = tempInstance;
}
return instance;
}
}
}
Я дважды проверил блокировку, используя логическое значение (т.используя примитив, чтобы избежать ленивой инициализации):
Синглтон с использованием логического значения не работает.Порядок операций, наблюдаемый между различными потоками, не гарантируется, если вы не преодолеете барьер памяти.Другими словами, как видно из второго потока,created = true
может быть выполнено раньше instance= new Singleton();
Я не совсем понимаю, зачем существует куча шаблонов реализации блокировки с двойной проверкой (видимо, для обхода особенностей компилятора на разных языках).Статья в Википедии на эту тему показывает наивный метод и возможные способы решения проблемы, но ни один из них не является таким простым (на C#):
public class Foo
{
static Foo _singleton = null;
static object _singletonLock = new object();
public static Foo Singleton
{
get
{
if ( _singleton == null )
lock ( _singletonLock )
if ( _singleton == null )
{
Foo foo = new Foo();
// Do possibly lengthy initialization,
// but make sure the initialization
// chain doesn't invoke Foo.Singleton.
foo.Initialize();
// _singleton remains null until
// object construction is done.
_singleton = foo;
}
return _singleton;
}
}
В Java вместо lock() можно использовать синхронизированную(), но по сути это та же самая идея.Если существует возможная несогласованность в том, когда присваивается одноэлементное поле, то почему бы просто не использовать сначала локальную переменную, а затем назначить одноэлементное поле в последний возможный момент перед выходом из критической секции?Я что-то пропустил?
@michael-borgwardt утверждает, что в C# и Java статическое поле инициализируется только один раз при первом использовании, но что поведение зависит от языка.И я часто использовал этот шаблон для ленивой инициализации свойства коллекции (например,пользователь.Сессии).
Я дважды проверил блокировку, используя логическое значение (т.используя примитив, чтобы избежать ленивой инициализации):
private static Singleton instance;
private static boolean created;
public static Singleton getInstance() {
if (!created) {
synchronized (Singleton.class) {
if (!created) {
instance = new Singleton();
created = true;
}
}
}
return instance;
}