То, что изначально казалось проблемой с простым решением, оказалось довольно интересной задачей.
У меня есть класс, который поддерживает внутренне-ориентированный, поточно-ориентированный набор (с использованием 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
, т. Е. Число, для которого берется квадратный корень, оценивалось как очень маленькое отрицательное число.)