Опять дважды проверил блокировку и C # - PullRequest
5 голосов
/ 13 июня 2011

Недавно я проводил рефакторинг некоторых моих кодов на C # и обнаружил несколько проверенных методов блокировки. Тогда я не знал, что это плохая практика, и я действительно хочу от нее избавиться.

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

Мне было интересно, будет ли приемлемым решением использовать ReaderWriterLockSlim в C # и ввести UpgradeableReadLock перед первой проверкой, а затем, если необходимо, ввести блокировку записи для инициализации. Вот что я имею в виду:

public class LazyInitialized
{
    private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();

    private volatile WeakReference _valueReference = new WeakReference(null);
    public MyType Value
    {
        get
        {
            MyType value = _valueReference.Target as MyType;
            _lock.EnterUpgradeableReadLock();
            try
            {
                if (!_valueReference.IsAlive) // needs initializing
                {
                    _lock.EnterWriteLock();
                    try
                    {
                        if (!_valueReference.IsAlive) // check again
                        {
                            // prevent reading the old weak reference
                            Thread.MemoryBarrier(); 
                            _valueReference = new WeakReference(value = InitializeMyType());
                        }
                    }
                    finally
                    {
                        _lock.ExitWriteLock();
                    }
                }
            }
            finally
            {
                _lock.ExitUpgradeableReadLock();
            }
            return value;
        }       
    }

    private MyType InitializeMyType()
    {
        // code not shown    
    }
}

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

Я также прочитал статью здесь , в которой говорится, что энергозависимость приводит к тому, что барьеры памяти автоматически вставляются перед чтением и после записи, поэтому я предполагаю, что для проверки будет достаточно только одного определенного вручную барьера между чтением и записью что объект _valueReference правильно прочитан. Буду рад вашим советам и критике за использование этого подхода.

Ответы [ 2 ]

2 голосов
/ 13 июня 2011

Чтобы подчеркнуть точку зрения @Mannimarco: если это единственная точка доступа к значению, и она выглядит таким образом, то вся ваша установка ReaderWriterLockSlim не лучше, чем простой подход Monitor.Enter / Monitor.Leave. Это намного сложнее, хотя.

Поэтому я считаю, что следующий код эквивалентен по функциям и эффективности:

private WeakReference _valueReference = new WeakReference(null);
private object _locker = new object();

public MyType Value
{    
  get
  {    
    lock(_locker)  // also provides the barriers
    {
        value = _valueReference.Target;

        if (!_valueReference.IsAlive)
        {
            _valueReference = new WeakReference(value = InitializeMyType());
        }
        return value; 
    }
  }    
}
2 голосов
/ 13 июня 2011

Предупреждение: только один поток может одновременно войти в режим UpgradeableReadLock. Проверьте ReaderWriterLockSlim . Таким образом, если потоки накапливаются, когда первый поток входит в режим записи и создает объект, у вас будет узкое место, пока резервная копия (надеюсь) не будет решена. Я бы серьезно предложил использовать статический инициализатор, это облегчит вашу жизнь.

РЕДАКТИРОВАТЬ: В зависимости от того, как часто объект должен быть воссоздан, я бы на самом деле предложил использовать класс Monitor и его методы Wait и Pulse. Если значение необходимо воссоздать, попросите потоки подождать объект и передайте другому объекту Pulse, чтобы рабочий поток знал, что ему нужно проснуться и создать новый объект. Как только объект будет создан, PulseAll позволит всем потокам читателя проснуться и захватить новое значение. (в теории)

...