I have a property with a backing field which I want to make thread safe (get and set). The get and set method has no logic except the setting and returning.
I think there are two ways to capsule the logic in the property self (volatile and lock). Is my understanding of the two's correct or have I make any mistakes?
Below are my examples:
public class ThreadSafeClass
{
// 1. Volatile Example:
private volatile int _processState_1;
public int ProcessState_1
{
get { return _processState_1; }
set { _processState_1 = value; }
}
// 2. Locking Example:
private readonly object _processState_2Lock = new object();
private int _processState_2;
public int ProcessState_2
{
get
{
lock (_processState_2Lock)
{
return _processState_2;
}
}
set
{
lock (_processState_2Lock)
{
_processState_2 = value;
}
}
}
}
For more information see Threading in C# (part 4) by J. Albahari:
Synchronization constructs can be divided into four categories:
Simple blocking methods:
These wait for another thread to finish or for a period of time to elapse. Sleep
, Join
, and Task.Wait
are simple blocking methods.
Locking constructs:
These limit the number of threads that can perform some activity or execute a section of code at a time. Exclusive locking constructs are most common — these allow just one thread in at a time, and allow competing threads to access common data without interfering with each other. The standard exclusive locking constructs are lock
(Monitor.Enter
/Monitor.Exit
), Mutex
, and SpinLock
. The nonexclusive locking
constructs are Semaphore
, SemaphoreSlim
, and the reader/writer
locks.
Signaling constructs:
These allow a thread to pause until receiving a notification from another, avoiding the need for inefficient polling. There are two commonly used signaling devices: event wait handles and Monitor’s Wait/Pulse methods. Framework 4.0 introduces the CountdownEvent
and Barrier
classes.
Non-blocking synchronization constructs:
These protect access to a common field by calling upon processor primitives. The CLR and C# provide the following nonblocking constructs: Thread.MemoryBarrier
, Thread.VolatileRead
, Thread.VolatileWrite
, the volatile
keyword, and the Interlocked
class.
The volatile
keyword:
The volatile keyword instructs the compiler to generate an acquire-fence on every read from that field, and a release-fence on every write to that field. An acquire-fence prevents other reads/writes from being moved before the fence; a release-fence prevents other reads/writes from being moved after the fence. These “half-fences” are faster than full fences because they give the run-time and hardware more scope for optimization.
As it happens, Intel’s X86 and X64 processors always apply acquire-fences to reads and release-fences to writes — whether or not you use the volatile keyword — so this keyword has no effect on the hardware if you’re using these processors. However,
volatile
does have an effect on optimizations performed by the compiler and the CLR — as well as on 64-bit AMD and (to a greater extent) Itanium processors. This means that you cannot be more relaxed by virtue of your clients running a particular type of CPU.
The effect of applying volatile to fields can be summarized as follows:
First instruction Second instruction Can they be swapped?
Read Read No
Read Write No
Write Write No (The CLR ensures that write-write operations are never swapped, even without the volatile keyword)
Write Read Yes!
Notice that applying volatile doesn’t prevent a write followed by a read from being swapped, and this can create brainteasers. Joe Duffy illustrates the problem well with the following example: if Test1
and Test2
run simultaneously on different threads, it’s possible for a and b to both end up with a value of 0 (despite the use of volatile on both x
and y
):
class IfYouThinkYouUnderstandVolatile
{
volatile int x, y;
void Test1() // Executed on one thread
{
x = 1; // Volatile write (release-fence)
int a = y; // Volatile read (acquire-fence)
...
}
void Test2() // Executed on another thread
{
y = 1; // Volatile write (release-fence)
int b = x; // Volatile read (acquire-fence)
...
}
}
The MSDN documentation states that use of the volatile keyword ensures that the most up-to-date value is present in the field at all times. This is incorrect, since as we’ve seen, a write followed by a read can be reordered.
This presents a strong case for avoiding volatile: even if you understand the subtlety in this example, will other developers working on your code also understand it? A full fence between each of the two assignments in Test1
and Test2
(or a lock) solves the problem.
The volatile
keyword is not supported with pass-by-reference arguments or captured local variables: in these cases you must use the VolatileRead
and VolatileWrite
methods.