Page 2 of 5
Methods to make a thread safe
There are three general approaches to making code threadsafe:
Write re-entrant code
If you only use local variables stored on the stack then restarting the function with another thread automatically saves the state of any original invocation. Restricting storage to the stack isn’t always easy so .NET also provides thread-local storage which ensures that each thread has its own private copy of any variables it used.
Re-entrant code is a good approach if you need a function that can be used by multiple threads to do a task, but it forbids any sharing of resources which would render the code non-re-entrant.
This is a very general approach and it works by allowing only one thread to access a shared resource at a time. There are lots of facilities provided within .NET to allow you to implement mutual exclusion and more complicates access methods. The big problem with access control is knowing when a resource is in use by a thread and when it is free. Access control also brings with it problems connected with what happens when a thread wants to use a resource which is locked.
An atomic operation is one that cannot be interrupted by a change of thread. That is, once started an atomic operation will complete without yielding to another thread. In general which operations are atomic is defined by the hardware so this isn’t a particularly stable multiplatform solution. However it is one of the easiest of approaches to thread safety.
Atomic operations don't help much with the problem of sharing global resources but they are a step towards making sure that we are working in terms of complete updates as written in the code.
Atomic operations and volatility
Before moving onto more sophisticated ideas let’s consider some of the issues of low level data access.
The .NET CLR guarantees that all native integer operations are atomic in the sense that these cannot be interrupted by a task swap in mid operation.
This avoids the problem of a variable being changed by another thread in the middle of an operation so resulting in invalid or inconsistent data. The problem with this definition is that the native integer size varies according to machine type.
an atomic operation? It all depends on the machine it is run on.
If you want to be sure an operation is atomic then use the operations provided by the Interlocked class.
will add one to count without any risk that the process will be interrupted by another thread using a similar Interlocked operation.
Notice that it can potentially be interrupted by standard non-interlocked operation.
For this approach to work all of the threads have to use nothing but Interlocked operations to access shared resources.
The advantage of Interlocked is simplicity; its disadvantage is that it is limited to the methods provided. In most cases it is much better to use a lock based on a Monitor, as described later.
There is another strange problem associated with the way a variable changes its state or not.
During a non-atomic operation a variable might change its state due to another thread. However, it is possible that the compiler and or machine architecture may make the assumption that a variable can’t change if the current thread doesn’t change it. This allows variables to be cached in fast memory but the cached copy may not reflect the true value of the variable stored in main memory.
In practice this problem doesn’t happen at all often but you can declare a variable as volatile if you need the compiler to check that it hasn’t been changed by another thread. In practice it usually isn’t necessary to use volatile as, once again, if you use a lock it performs a volatile read at the start and a volatile write at the end making sure everything is up-to-date.