При реализации потоковой очереди или списка требуется ли блокировка перед возвратом Count? - PullRequest
4 голосов
/ 29 октября 2010

При реализации потокобезопасного списка или очереди; требуется ли заблокировать свойство List.Count перед возвратом Count, т.е.:

//...
public int Count 
{
    lock (_syncObject)
    {
       return _list.Count;
    }
}
//...

Нужно ли делать блокировку из-за исходной переменной _list.Count, возможно, не является переменной volatile?

Ответы [ 3 ]

5 голосов
/ 29 октября 2010

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

Но обратите внимание, что использование этого свойства Count проблематично, любой вызывающий код не может действительно полагаться на него:

if (myStore.Count > 0)  // this Count's getter  locks internally 
{
    var item = myStore.Dequeue(); // not safe, myStore could be empty
}

Поэтому вы должны стремиться к разработке, в которой проверка счетчика и действиеони комбинируются:

ItemType GetNullOrFirst()
{
    lock (_syncObject)
    {
       if (_list.Count > 0)
       {
           ....
       }
    }
}

Дополнительно:

Обязательно ли делать блокировку из-за исходной переменной _list.Count, возможно, не является переменной volatile?

_list.Count - это не переменная, а свойство.Он не может быть помечен как изменчивый.Будь он потокобезопасным, зависит от кода получателя свойства, но обычно он будет безопасным.Но ненадежный.

2 голосов
/ 29 октября 2010

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

count is n
thread asks for count
Count returns n
another thread dequeues
count is n - 1
thread asking for count sees count is n when count is actually n - 1
2 голосов
/ 29 октября 2010

Это зависит.

Теперь, теоретически, поскольку базовое значение не является потокобезопасным, практически все может пойти не так из-за того, что делает реализация. Однако на практике это чтение из 32-битной переменной, поэтому оно гарантированно является атомарным. Следовательно, это может быть устаревшим, но это будет реальное устаревшее значение, а не мусор, вызванный чтением половины значения до изменения и другой половины после него.

Итак, вопрос в том, имеет ли значение устаревание? Возможно, это не так. Чем больше вы можете смириться со устареванием, тем лучше для производительности (потому что, чем больше вы можете с этим мириться, тем меньше нужно делать, чтобы убедиться, что у вас нет ничего устаревшего). Например. если вы выставляете счет в пользовательском интерфейсе, и коллекция быстро меняется, то времени, которое нужно человеку, чтобы прочитать число и обработать его в своем мозгу, будет достаточно, чтобы оно все равно устарело, и, следовательно, несвежий.

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

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

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

Здесь важно то, что даже с классом, который является поточно-ориентированным во всех отношениях, лучшее, что может гарантировать, - это то, что каждая операция будет новой (хотя даже это не может быть гарантировано; в действительности, когда задействовано более одного ядра) «свежий» начинает терять смысл, поскольку могут происходить изменения, близкие к действительно одновременным), и что каждая операция не помещает объект в недопустимый сейф. Тем не менее, можно писать код, не поддерживающий потоки, с помощью потоковобезопасных объектов (на самом деле очень многие объекты, не поддерживающие потоки, состоят из целых чисел, строк и объектов типа bool или объектов, которые в свою очередь состоят из них, и каждый из них сам по себе является потокобезопасным) .

Одной вещью, которая может быть полезна с изменяемыми классами, предназначенными для многопоточного использования, являются синхронизированные методы Try. Например. чтобы использовать List в качестве стека, нам нужны следующие операции:

  1. Проверьте, пуст ли список, и сообщит об ошибке, если он есть.
  2. Получить значение "top".
  3. Удалить верхнее значение.

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

...