Pull to refresh

Comments 17

lock (_lock)
            {
                return _blob ?? (_blob = new Blob());
            }

Вариант короткий и выразительный, но неоптимальный с точки зрения производительности, т.к. lock не бесплатный, а нужен только для инициализации. Лучше делать проверку до lock, чтобы избежать ненужного захода в критическую секцию, а также внутри, чтобы избежать повторной инициализации, если другой поток успел проинциализировать поле после первой проверки текущим потоком. Кода больше, но работать будет быстрее.

Double check locking это. Сначала проверяете в read потом уже во write.
Вместо первого лока можно использовать проверку атомика

UFO just landed and posted this here
А для чего создается отдельный lock? Почему нельзя сделать lock(this)? C C# работаю не так давно, поэтому это не критика, а запрос информации.
Для большей безопасности. При локе на this нет гарантий, что кто-то ещё не решит также использовать ваш объект в качестве ключа, что может привести к внезапным дедлокам.

Потому что на данный объект может сделать такой же лок создатель этого объекта, да и по сути кто угодно. Тогда произойдёт deadlock.

Дедлок это одна из причин, код класса не контролирует кто блокирует объект извне. Отдельный лок позволяет полностью передать контроль над блокировкой коду класса.

Другая причина в том, что если если lock(this) используется в разных методах одного класса, то выполнение одного метода будет блокировать выполнение других в случае, если обращение происходит из другого потока. Это не всегда эффективно, например, когда объект хранит несколько независимых друг от друга состояний.

Кстати, чтобы два раза не вставать, еще хуже это lock(typeof(YourType)), так как typeof() возвращает один и тот же экземпляр типа Type независимо от того, откуда он вызван, то контроль над тем кто и когда блокирует ваш код, теряется полностью.
Спасибо за ответы, я понял суть возможной проблемы. Я не использую долгосрочные блокировки, поэтому про дедлок не подумал.
LazyInitializer.EnsureInitialized(ref _blob)

Не гарантирует единичную инициализацию объекта. Если ломануться 40 потоков, то может получиться 39 инстансов мусора и один победитель.

Правильный вариант для многопоточности, который не очень то и удобен:
// System.Threading.LazyInitializer
private static T EnsureInitializedCore<T>(ref T target, ref bool initialized, ref object syncLock, Func<T> valueFactory)
{
	object obj = syncLock;
	if (obj == null)
	{
		object obj2 = new object();
		obj = Interlocked.CompareExchange(ref syncLock, obj2, null);
		if (obj == null)
		{
			obj = obj2;
		}
	}
	object obj3 = obj;
	lock (obj3)
	{
		if (!Volatile.Read(ref initialized))
		{
			target = valueFactory();
			Volatile.Write(ref initialized, true);
		}
	}
	return target;
}
Ну вообще-то в MSDN есть корректный пример использования в многопоточной среде:
ExpensiveData _data = null;  
bool _dataInitialized = false;  
object _dataLock = new object();  

//  ...  

ExpensiveData dataToUse = LazyInitializer.EnsureInitialized(ref _data, ref _dataInitialized, ref _dataLock);  
Если есть вероятность, что к данному ресурсу могут обращаться сразу несколько потоков, нам стоит сделать его потокобезопасным.

Оператор lock получает взаимоисключающую блокировку заданного объекта перед выполнением определенных операторов, а затем снимает блокировку.

Если допустимо создание нескольких копий ресурса, который надо инициализовать, параллельно в нескольких потоках, то можно обойтись без lock:
class Test
{
    private volatile Blob _blob = null;

    public Blob BlobData
    {
        get
        {
              Blob existing_blob = _blob;
              if(existing_blob!=null) return existing_blob;
              Blob candidate_blob = new Blob();
              Interlocked.CompareExchange<Blob>(_blob,candidate_blob,null);
              return _blob;
          }
    }
}

Суть кода проста: если ресурс ещё мог не быть создан (ссылка на него — null), то оптимистически создаем новый ресурс и пытаемся сохранить его ссылку атомарной операцией условной записи при условии равенства существующей ссылки значению null.
Если не получилось, то созданный нами объект становится добычей сборщика мусора, но в любом случае после этой операции поле ссылки содержит уже инициализованный (нами или кем-то ещё) ресурс — его и возвращаем.
Здесь приведен вариант для случая, когда class Blob не реализует IDisposable, иначе нужно модифицировать код так, чтобы он вызывал Dispose() у лишнего созданного объекта.

У вас в коде два чтения volatile-переменной, но можно и одним обойтись, если использовать Volatile.Read вместо volatile.


А ещё лучше — использовать LazyInitializer, который делает примерно то же самое.

У вас в коде два чтения volatile-переменной, но можно и одним обойтись, если использовать Volatile.Read вместо volatile.

И с volatile тоже можно сэкономить одно чтение: сделать две последние строчки до всех закрывающих фигурных скобок такими:
 existing_blob=Interlocked.CompareExchange<Blob>(_blob,candidate_blob,null);
 return existing_blob??candidate_blob;

Но не вижу особого смысла оптимизировать этот кусок, выполняющийся в очень короткий момент в начале и, чаще всего, ровно один раз.
А ещё лучше — использовать LazyInitializer, который делает примерно то же самое.

Бесспорно — без своего велосипеда всегда обойтись лучше (правда, не совсем всегда получается: LazyInitializer появился только в Framework 4.0, а по жизни встречается и более старый код).
Но раз уж автор статьи тут решил собрать максимальную коллекцию, как вообще можно делать отложенную инициализацию, то я добавил к ней ещё один вариант.

Так то с Lazy всё не так просто. При создании нужно указать правильные параметры, чтобы не огрести при исключении внутри инициализатора.

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

Lazy — похожая сделка с дьяволом. Сплошь и рядом люди засовывают в lazy инициализации справочников из БД, иногда и каскадных (!). Чтобы не думать, как и где брать данные — само приедет, только свистни. Что при этом происходит в самой БД, когда вся эта волынка начинает хаотично лезть в базу, выгружая тысячи записей для пары-тройки референсных значений (и если еще и под внешней транзакцией) — описать приличными словами сложно.

Используйте MISRA-подходы даже в энтерпрайз-приложениях. Максимум создавайте статикой. Не плодите объекты, чтобы их сейчас же выбросить. Не бросайте мусор. Дворник не так проворен. Поддерживайте гигиену работы с БД. Отдавайте себе отчет, чего стоит создание объекта класса и вызов его методов. Не прячьте в геттерах положенный в картонную коробку кирпич — кто-нибудь обязательно пнет его ногой. Может даже, по прошествии времени это будете Вы сами ;)

Лично мне было бы интересно почитать про подобные подходы применительно к c#, никто не возьмётся написать статью?

Sign up to leave a comment.

Articles