Multithreading synchronization

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.

Note

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);

Locks

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);
    }
}

Signaling locks

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.

Drawbacks of locks

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.187.62