Как легко сделать этот поток свойств счетчика безопасным?

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

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

Ответ 1

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

  • Получить значение DoneCounter.
  • Добавить 1
  • Сохраните результат в 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); }

Ответ 2

Использование класса 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); }
    }
}

Ответ 4

Что именно вы пытаетесь сделать с помощью счетчиков? Замки на самом деле мало чем отличаются от целочисленных свойств, поскольку чтение и запись целых чисел являются атомарными или без блокировки. Единственное преимущество, которое можно получить от замков, - это добавление барьеров памяти; можно добиться такого же эффекта, используя 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. То, что понадобилось бы, было бы методом, который выполнял бы получение, вычисление и установку без каких-либо промежуточных операций. Это можно сделать тремя способами:

  • Получить и сохранить блокировку во время всей операции
  • Используйте Threading.Interlocked.Increment для добавления значения в _Counter
  • Используйте цикл Threading.Interlocked.CompareExchange для обновления _Counter

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

Ответ 5

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

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

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