Какой стандартный подход к выполнению вычислений потокобезопасным? - PullRequest
1 голос
/ 09 октября 2009

То, что изначально казалось проблемой с простым решением, оказалось довольно интересной задачей.

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

Один пример:

public double StandardDeviation {
    get {
        return Math.Sqrt((Sum2 - ((Sum * Sum) / Count)) / Count);
    }
}

Теперь я тщательно проверил это вычисление, проверив 10 000 значений в коллекции и проверив стандартное отклонение при каждом обновлении. Работает нормально ... в однопоточном сценарии.

Однако возникает проблема в многопоточном контексте наших сред разработки и производства. Кажется, что это число каким-то образом иногда возвращается NaN, прежде чем быстро вернуться к реальному числу. Естественно, это должно происходить из-за отрицательного значения, переданного Math.Sqrt. Я могу только представить, что это происходит, когда в середине расчета одно из значений, используемых в расчете, обновляется отдельным потоком.

Я мог бы сначала кэшировать значения:

int C = this.Count;
double S = this.Sum;
double S2 = this.Sum2;

return Math.Sqrt((S2 - (S * S) / C) / C);

Но тогда Sum2 все еще может обновляться, например, после установки S = this.Sum, что еще раз компрометирует вычисления.

Я мог бы поставить lock вокруг всех точек в коде, где обновляются эти значения:

protected void ItemAdded(double item) {
    // ...

    lock (this.CalculationLock) {
        this.Sum += item;
        this.Sum2 += (item * item);
    }
}

Тогда, если бы я lock на этом же объекте при вычислении StandardDeviation, я подумал , это, наконец, решило бы проблему. Это не так. Значение все еще приходит как NaN на мимолетной, нечастой основе.

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


РЕДАКТИРОВАТЬ : Оказывается, здесь у нас есть пример проблемы, когда сначала казалось, что есть только одно возможное объяснение, когда, в конце концов, проблема была с чем-то другим полностью.

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

Ничего не сработало; это гнусное NaN продолжало появляться. Поэтому я решил вывести все значения в коллекции на консоль всякий раз, когда StandardDeviation возвращает NaN ...

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

Официально: я сгорел от арифметики с плавающей запятой. (Все значения были одинаковыми, поэтому значение radicand в StandardDeviation, т. Е. Число, для которого берется квадратный корень, оценивалось как очень маленькое отрицательное число.)

Ответы [ 5 ]

3 голосов
/ 09 октября 2009

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

protected void ItemAdded(double item) {
    // ...

    lock (this.CalculationLock) {
        this.Sum += item;
        this.Sum2 += (item * item);
    }
}

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

Это точно что вы должны сделать для правильности. Если это не работает для вас, я бы посоветовал вам пропустить сценарий обновления - или у вас есть другая проблема (например, Sum или Sum2, иногда являющиеся NaN, или неожиданное значение из-за какой-то другой расы состояние).

2 голосов
/ 09 октября 2009

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

Самый простой и самый «стандартный» подход заключается в использовании той же блокировки, которую вы используете для синхронизации вставок и удалений. По мере добавления или удаления обновляйте Sum и Count. Затем используйте эту же блокировку для синхронизации доступа к Sum и Count в функции StandardDeviation.

1 голос
/ 09 октября 2009

Если, как вы упомянули в комментариях, производительность здесь имеет высокий приоритет, вам следует рассмотреть возможность синхронизации всего доступа к базовым данным объекта с ReaderWriterLockSlim (или более старым ReaderWriterLock, если вы используете 2.0 Framework) вместо Monitor (с помощью lock операторов), чтобы вычисления не блокировали друг друга.

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

1 голос
/ 09 октября 2009

Вы можете кэшировать весь список, а затем работать с кэшированной версией. Примерно так:

var copy = currentList.ToArray();
var sum = Sum(copy)
var sum2 = Sum2(copy)
return sum * sum2... whatever

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

0 голосов
/ 09 октября 2009

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

public double StandardDeviation
{
  get
  {
    lock (_lockObject)
    {
       return Math.Sqrt((Sum2 - ((Sum * Sum) / Count)) / Count);
    }
  }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...