Как легко сделать этот счетчик потокобезопасным? - PullRequest
11 голосов
/ 26 января 2012

У меня есть определение свойства в классе, где у меня есть только счетчики, это должно быть поточно-ориентированным, и это не потому, что get и set не в той же блокировке, как это сделать?

    private int _DoneCounter;
    public int DoneCounter
    {
        get
        {
            return _DoneCounter;
        }
        set
        {
            lock (sync)
            {
                _DoneCounter = value;
            }
        }
    }

Ответы [ 5 ]

23 голосов
/ 26 января 2012

Если вы хотите реализовать свойство таким образом, чтобы DoneCounter = DoneCounter + 1 гарантированно не зависело от условий гонки, этого нельзя сделать при реализации свойства.Эта операция не является атомарной, на самом деле это три отдельных шага:

  1. Получить значение DoneCounter.
  2. Добавить 1
  3. Сохранить результат в DoneCounter.

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

Ответственность за безопасность потоков лежит на человеке, использующем ваш объект.В общем, ни один класс, имеющий поля или свойства для чтения / записи, не может быть таким образом «поточно-ориентированным».Однако, если вы можете изменить интерфейс класса так, чтобы установщики не были нужны, то можно сделать его более потокобезопасным.Например, если вы знаете, что DoneCounter только увеличивает и уменьшает значение, вы можете реализовать его следующим образом:

private int _doneCounter;
public int DoneCounter { get { return _doneCounter; } }
public int IncrementDoneCounter() { return Interlocked.Increment(ref _doneCounter); }
public int DecrementDoneCounter() { return Interlocked.Decrement(ref _doneCounter); }
4 голосов
/ 26 января 2012

Использование класса Interlocked обеспечивает атомарные операции, то есть по сути потокобезопасные, как в этом примере LinqPad:

void Main()
{
    var counters = new Counters();
    counters.DoneCounter += 34;
    var val = counters.DoneCounter;
    val.Dump(); // 34
}

public class Counters
{
    int doneCounter = 0;
    public int DoneCounter
    {
        get { return Interlocked.CompareExchange(ref doneCounter, 0, 0); }
        set { Interlocked.Exchange(ref doneCounter, value); }
    }
}
2 голосов
/ 26 января 2012

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

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

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

2 голосов
/ 26 января 2012

Что именно вы пытаетесь сделать со счетчиками? Блокировки на самом деле мало что делают с целочисленными свойствами, поскольку чтение и запись целых чисел являются атомарными с блокировкой или без нее. Единственное преимущество, которое можно получить от блокировок, - это добавление барьеров памяти; можно достичь того же эффекта, используя Threading.Thread.MemoryBarrier() до и после чтения или записи общей переменной.

Я подозреваю, что ваша настоящая проблема в том, что вы пытаетесь сделать что-то вроде "DoneCounter + = 1", которое - даже с блокировкой - будет выполнять следующую последовательность событий:

  Acquire lock
  Get _DoneCounter
  Release lock
  Add one to value that was read
  Acquire lock
  Set _DoneCounter to computed value
  Release lock

Не очень полезно, так как значение может меняться между get и set. То, что было бы необходимо, это метод, который будет выполнять получение, вычисление и установку без каких-либо промежуточных операций. Это можно сделать тремя способами:

  1. Получение и удержание блокировки в течение всей операции
  2. Используйте Threading.Interlocked.Increment, чтобы добавить значение в _Counter
  3. Используйте цикл Threading.Interlocked.CompareExchange для обновления _Counter

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

0 голосов
/ 26 января 2012

Вы можете объявить переменную _DoneCounter как "volatile", чтобы сделать ее поточно-ориентированной.Смотрите это:

http://msdn.microsoft.com/en-us/library/x13ttww7%28v=vs.71%29.aspx

...