When dealing with multiple threads, data access in fields and properties must be synchronized, otherwise inconsistent data states may occur. Although CLR guarantees low-level data consistency by always performing a read/write operation, such as an atomic operation against any field or variable, when multiple threads use multiple variables, it may happen that during the write operation of a thread, another thread could also write the same values, creating an inconsistent state of the whole application.
First, let's take care of field initialization when dealing with multithreading. Here is an interesting example:
// a static variable without any thread-access optimization public static int simpleValue = 10; // a static variable with a value per thread instead per the whole process [ThreadStatic] public static int staticValue = 10; //a thread-instantiated value public static ThreadLocal<int> threadLocalizedValue = new ThreadLocal<int>(() => 10); static void Main(string[] args) { // let's start 10 threads for (int i = 0; i < 10; i++) new Thread(IncrementVolatileValue).Start(); Console.ReadLine(); } private static void IncrementVolatileValue(object state) { // let's increment the value of all variables staticValue += 1; simpleValue += 1; threadLocalizedValue.Value += 1; Console.WriteLine("Simple: {0} Localized: {1} Static: {2}", simpleValue, threadLocalizedValue.Value, staticValue); }
Here is the console output:
Simple: 18 Localized: 11 Static: 1 Simple: 19 Localized: 11 Static: 1 Simple: 18 Localized: 11 Static: 1 Simple: 18 Localized: 11 Static: 1 Simple: 19 Localized: 11 Static: 1 Simple: 18 Localized: 11 Static: 1 Simple: 19 Localized: 11 Static: 1 Simple: 19 Localized: 11 Static: 1 Simple: 19 Localized: 11 Static: 1 Simple: 20 Localized: 11 Static: 1
The preceding code example simply incremented three different integer variables by 1
. The result shows how different setups of such variable visibility and thread availability will produce different values, although they should all be virtually equal.
The first value (simpleValue
) is a simple static integer that when incremented by 1
in all ten threads creates some data inconsistency. The value should be 20 for all threads—in some threads, the read value is 18, in some other 19, and in only one other thread is 20. This shows how setting a static value in multithreading without any thread synchronization technique will easily produce inconsistent data.
The second value (the staticValue
) is outputted in the middle of the example output. The usage of the ThreadStaticAttribute
legacy breaks the field initialization and duplicates the value for each calling thread, actually creating 10 copies of such an integer. Indeed, all threads write the same value made by 10 plus 1.
The most decoupled value is obtained by the third value (threadLocalizedValue
), shown at the right of the example output. This generic compliant class (ThreadLocal<int>) behaves as the ThreadStaticAttribute
usage by multiplying the field per calling thread with the added benefit of initializing such values with an anonymous function at each thread startup.
C# gives us the volatile keyword that signals to JIT that the field access must not be optimized at all. This means no CPU register caching, causing all threads to read/write the same value available in the main memory. Although this may seem to be a sort of magic synchronization technique, it is not; it does not work at all. Accessing a field in a volatile manner is a complex old-style design that actually does not have reason to be used within CLR-powered languages.
For more information, please read this article by Eric Lippert, the Chief Programmer of the C# compiler team in Microsoft, at http://blogs.msdn.com/b/ericlippert/archive/2011/06/16/atomicity-volatility-and-immutability-are-different-part-three.aspx.
More than the standard atomic operation given by CLR to any field, only for primitive types (often limited to int
and long
), CLR also offers a memory fence, such as field access utility named Interlocked. This can make low-level memory-fenced operations such as increment, decrement, and exchange value. All those operations are thread-safe to avoid data inconsistency without using locks or signals. Here is an example:
//increment of 1 Interlocked.Increment(ref value); //decrement of 1 Interlocked.Decrement(ref value); //increment of given value Interlocked.Add(ref value, 4); //substitute with given value Interlocked.Exchange(ref value, 14);
Different synchronization techniques and lock objects exist within CLR and outside of Windows itself. A lock is a kind of flag that stops the execution of a thread until another one releases the contended resources. All locks and other synchronization helpers will prevent threads from working on bad data, while adding some overhead.
In .NET, multiple classes are available to handle locks. The easiest is the Monitor
class, which is also usable with the built-in keyword lock
(SyncLock
in VB). The Monitor lock allows you to lock access to a portion of code. Here is an example:
private static readonly object flag = new object(); private static void MultiThreadWork() { //serialize access to this portion of code //using the keyword lock (flag) { //do something with any thread un-safe resource } //this code actually does the same of the lock block above try { //take exclusive access Monitor.Enter(flag); //do something with any thread un-safe resource } finally { //release exclusive access Monitor.Exit(flag); } }
All those locks that inherit the WaitHandle
class are signaling locks. Instead of locking the execution code, they send messages to acknowledge that a resource has become available. They are all based on a Window kernel handle, the SafeWaitHandle
, this is different from the Monitor class
that works in user mode because it is made entirely in managed code from CLR. Such low-level heritage in the WaitHandle
class hierarchy adds the ability to cross AppDomains by reference, inheriting from the MashalByRefObject
class.
More powerful than the Monitor
class, the Mutex
class inherits all features from the Monitor
class, adding some interesting features, such as the ability to synchronize different processes working at the operating-system level. This is useful when dealing with multi-application synchronization needs.
Following is a code example of the Mutex
class usage. We will create a simple console application that will await an operating-system level synchronization lock with the global name of MUTEX_001
.
Please start multiple instances of the following application to test it out:
static void Main(string[] args) { Mutex mutex; try { //try using the global mutex if already created mutex = Mutex.OpenExisting("MUTEX_001"); } catch (WaitHandleCannotBeOpenedException) { //creates a new (not owned) mutex mutex = new Mutex(false, "MUTEX_001"); } Console.WriteLine("Waiting mutex..."); //max 10 second timeout to acquire lock mutex.WaitOne(); try { //you code here Console.WriteLine("RETURN TO RELEASE"); Console.ReadLine(); } finally { mutex.ReleaseMutex(); Console.WriteLine("Mutex released!"); } mutex.Dispose(); }
Like the Monitor
class, the Semaphore
class enables us to lock a specific code portion access. The unique (and great) difference is that instead of allowing a single thread to execute such a code-block, the Semaphore
class allows multiple threads all together. This class is a type of a limiter for limiting the resource usage.
In the following code example, we will see the Semaphore
class is configured to allow up to four threads to execute all together—other threads will be queued until some allowed thread ends its job:
class Program { static void Main(string[] args) { for (int i = 0; i < 100; i++) new Thread(AnotherThreadWork).Start(); Console.WriteLine("RETURN TO END"); Console.ReadLine(); } //4 concurrent threads max private static readonly Semaphore waiter = new Semaphore(4, 4); private static void AnotherThreadWork(object obj) { waiter.WaitOne(); Thread.Sleep(1000); Console.WriteLine("{0} -> Processed", Thread.CurrentThread.ManagedThreadId); waiter.Release(); } }
Other widely used signaling lock classes are ManualResetEvent
and the AutoResetEvent
class. The two implementations simply differ in terms of the manual or automatic switch of the signal state to a new value and back to the initial value.
The usage of those two classes is completely different when compared to all classes seen before, because instead of giving us the ability to serialize thread access of a code-block, these two classes act as flags giving the signal everywhere in our application to indicate whether or not something has happened.
For instance, we can use the AutoResetEvent
class to signal that we are doing something and let multiple threads wait for the same event. Later, once signaled, all such threads could proceed in processing without serializing the thread execution, for instance, when we use locks instead, like all others seen earlier, such as the Monitor
, Mutex
, or Semaphore
classes.
Here is a code example showing two threads, each signaling its completion by the manual or the automatic wait handle, during which the main code will await the thread's completion before reaching the end:
static void Main(string[] args) { new Thread(ManualSignalCompletion).Start(); new Thread(AutoSignalCompletion).Start(); //wait until the threads complete their job Console.WriteLine("Waiting manual one"); //this method simply asks for the signal state //indeed I can repeat this row infinite times manualSignal.WaitOne(); Console.WriteLine("Waiting auto one"); //this method asks for the signal state and also reset the value back //to un-signaled state, waiting again that some other code will //signal the completion //if I repeat this row, the program will simply wait forever autoSignal.WaitOne(); Console.WriteLine("RETURN TO END"); Console.ReadLine(); } private static readonly ManualResetEvent manualSignal = new ManualResetEvent(false); private static void ManualSignalCompletion(object obj) { Thread.Sleep(2000); manualSignal.Set(); } private static readonly AutoResetEvent autoSignal = new AutoResetEvent(false); private static void AutoSignalCompletion(object obj) { Thread.Sleep(5000); autoSignal.Set(); }
In this case, all such functionalities are overshot by the Task
class and the Task Parallel Library (TPL), which will be discussed throughout Chapter 4, Asynchronous Programming and Chapter 5, Programming for Parallelism.
Moreover, in .NET 4.0 or later, the Semaphore
and the ManualResetEvent
classes have alternatives in new classes that try to keep the behavior of the two previous ones by using a lighter approach. They are called ManualResetEventSlim
and SemaphoreSlim
.
Such new slim classes tend to limit access to the kernel mode handle by implementing the same logic in a managed way until possible (usually when a little time passes between signaling). This helps to execute faster than the legacy brothers do. Obviously, those objects lose the ability to cross boundaries of app domains or processes, as the WaitHandle
hierarchy usually does. The usage of those new classes is identical to previous ones, but with some simple method renaming.
New classes are available in .NET 4 or greater: CountdownEvent
and Barrier
. Similar to the two slim classes we just saw, these classes do not derive from the WaitHandle
hierarchy.
The Barrier
class, as the name implies, lets you program a software barrier. A barrier is like a safe point that multiple tasks will use as parking until a single external event is signaled. Once this happens, all threads will proceed together.
Although the Task
class offers better features in terms of continuation, in terms of more flexibility, the Barrier
class gives us the ability to use such logic everywhere with any handmade thread. On the other hand, the Task
class is great in continuation and synchronization of other Task
objects. Here is an example involving the Barrier
class:
private static readonly Barrier completionBarrier = new Barrier(4, OnBarrierReached); static void Main(string[] args) { new Thread(DoSomethingAndSignalBarrier).Start(1000); new Thread(DoSomethingAndSignalBarrier).Start(2000); new Thread(DoSomethingAndSignalBarrier).Start(3000); new Thread(DoSomethingAndSignalBarrier).Start(4000); Console.ReadLine(); } private static void DoSomethingAndSignalBarrier(object obj) { //do something Thread.Sleep((int)obj); //the timeout flowed as state object Console.WriteLine("{0:T} Waiting barrier...", DateTime.Now); //wait for other threads to proceed all together complationBarrier.SignalAndWait(); Console.WriteLine("{0:T} Completed", DateTime.Now); } private static void OnBarrierReached(Barrier obj) { Console.WriteLine("Barrier reached successfully!"); }
The following is the console output:
17:45:41 Waiting barrier... 17:45:42 Waiting barrier... 17:45:43 Waiting barrier... 17:45:44 Waiting barrier... Barrier reached successfully! 17:45:44 Completed 17:45:44 Completed 17:45:44 Completed 17:45:44 Completed
Similar to the Barrier
class, the CountdownEvent
class creates a backward timer to collect multiple activities and apply some continuation at the end:
private static readonly CountdownEvent counter = new CountdownEvent(100); static void Main(string[] args) { new Thread(RepeatSomething100Times).Start(); //wait for counter being zero counter.Wait(); Console.WriteLine("RETURN TO END"); Console.ReadLine(); } private static void RepeatSomething100Times(object obj) { for (int i = 0; i < 100; i++) { counter.Signal(); Thread.Sleep(100); } }
An interesting overview of all those techniques is available in this article, available on the MSDN website at http://msdn.microsoft.com/en-us/library/ms228964(v=vs.110).aspx.
Use lock techniques carefully. Always try to avoid any race condition that happens when multiple different threads are fighting each other in trying to access the same resource. This produces an inconsistent state and/or causes high resource usage too. When a race condition happens in the worst possible manner, there will be starvation for resources.
Starvation happens when a thread never gets access to CPU time because different threads of higher priority take all the time, sometimes also causing an operating system fault if a thread in a loop-state is unable to abort its execution when running at highest priority level (the same of the OS core threads). You can find more details on resource starvation at http://en.wikipedia.org/wiki/Resource_starvation.
With the wrong locking design, an application may fall in the deadlock state. Such a state occurs when multiple threads wait forever, each with the other, for the same resource or multiple resources without being able to exit this multiple lock state. Deadlock often happens in wrong relational database designs or due to the wrong usage of relational database inner lock techniques. More details on the deadlock state can be found at http://en.wikipedia.org/wiki/Deadlock.
Instead, with managed synchronization techniques such as spin-wait based algorithms (like the one within SemaphoreSlim
class), an infinite loop can occur, wasting CPU time forever and bringing the application into a state called livelock, which causes the process to crash for the stack-overflow condition, at a time. For more details on livelock, visit http://en.wikipedia.org/wiki/Deadlock#Livelock.
18.117.187.62