Вас волнуют 2 проблемы:
- Атомарная запись
- Чтение последнего записанного значения.
Что касается атомарной записи, то все, что размером с int и меньше гарантированно CLR пишется и читается атомарно. Все остальное может писаться и читаться не атомарно, если иное явно не указано в документации.
Например, строка - запись строки не атомарная операция (для понимания, запись ссылки на строку в указатель - это атомарная операция, конструирование строки самой - не атомарная), но это тип неизменемый, потому нельзя его прочитать в недописанном состоянии, каждое изменение в строке порождает новую строку, потому вы можете либо читать старое значение, либо новое, но не что то посередке.
То же самое про decimal.
Что про вторую проблему. Volatile может помочь с чтением последнего значения для некоторых типов, но не для всех. Для составных структур типа decimal volatile не помощник, потому что decimal занимает больше 4 байт и в теории не гарантируется атомарность записи на уровне CLR. Но, при этом, decimal реализован с прицелом на потокобезопастность, то есть это неизменяемый тип, как и строка, потому операции с decimal как бы не атомарные, но при этом потокобезопасные. И, я так понял, volatile имеет смысл применять для того, чтобы вырубить некоторые оптимзизации компилятора для простых типов (для честности, указатель на ссылочный тип тоже является простым типом), при этом для сложных структур наподобие decimal тех оптимизаций просто нет. То есть volatile к decimal не применить, так как decimal не укладывается в int и вроде как не атомарный, но при этом и нужды в volatile для decimal тоже нет.
Теперь про расстановку локов. Рассмотрим следующий код
class WithoutLock
{
private decimal _value;
public decimal Value
{
get{
return _value;
}
set{
_value = value;
Console.WriteLine(Value);
}
}
}
class WithLock
{
private object _locker = new object();
private decimal _value;
public decimal Value
{
get
{
lock (_locker)
{
return _value;
}
}
set
{
lock (_locker)
{
_value = value;
Console.WriteLine(Value);
}
}
}
}
Подумаем, в чем разница между этими классами?
Первое, это то, что при lock по время чтения значения, никто не может писать и наоборот. Второе - это то, то при записи числа, мы выведем в консоль то, что было записано.
То есть, в первом случае, между этими строками какой то поток может обновить свойство
_value = value;
// параллельные поток может сделать запись в Value пока этот поток между строчками находится.
Console.WriteLine(Value);
Чем это черевато? Тем, что без lock мы можем вывести уже новое значение, тогда как с lock мы выседем то же, что было в value.
Проведем полевые испытания
var without = new WithoutLock();
var with = new WithLock();
Parallel.For(1, 10, x=> without.Value = (decimal)x);
Console.WriteLine("----------------------");
Parallel.For(1, 10, x=> with.Value = (decimal)x);
Вывод
3
9
5
4
7
6
5
8
3
----------------------
3
8
1
4
2
5
6
7
9
Видно, что в первом случае, без lock, пятерка и тройка по 2 раза выведены, потому что в сеттере, до того, как мы что то вывели в консоль, значение value уже обновилось другим птотоком. С использвованием lock такого не происходит, потому как мы пишем и читаем внутри секции lock, которая не пускает другой поток.
Но! Стоит нам вынести вывод в консоль за lock
class WithLock
{
private object _locker = new object();
private decimal _value;
public decimal Value
{
get
{
lock (_locker)
{
return _value;
}
}
set
{
lock (_locker)
{
_value = value;
}
Console.WriteLine(Value);
}
}
}
Как мы получим ту же самую проблему на выходе
7
8
9
9
9
9
9
9
9
То есть наличие lock просто для чтения или записи не добавляет никакой потокобезопастности никуда в данном случае. Конечно, если у вас какая то более сложная операция записи будет (например, вам надо писать в 2 согласованные переменные или синхронизировать несколько операций), то тогда lock может стать полезным.