TPL Performance Improvements in .NET 4.5

Task.WaitAll and Task.WaitAny

Task’s waiting logic in .NET 4.5 has been changed. The performance gain for this change is most apparent when waiting on multiple Tasks, such as when using Task.WaitAll and Task.WaitAny.

Let’s explore the extent of this performance boost with this benchmark code for Task.WaitAll:

public static Tuple TestWaitAll(int ntasks)
        {
            Task[] tasks = new Task[ntasks];
            Action action = () => { };
            for (int i = 0; i < ntasks; i++) tasks[i] = new Task(action);
            Stopwatch sw = new Stopwatch();
            long startBytes = GC.GetTotalMemory(true);
            sw.Start();
            Task.WaitAll(tasks, 1);
            sw.Stop();
            long endBytes = GC.GetTotalMemory(true);
            GC.KeepAlive(tasks);
            return Tuple.Create(sw.ElapsedMilliseconds, endBytes - startBytes);
        }

The code above times the overhead of setting up a WaitAll for ntasks uncompleted Tasks, plus a one millisecond timeout. This test is admittedly less than perfectly precise, as the actual time before the WaitAll call times out could be anywhere from 1 millisecond to the scheduler quantum of the underlying operating system. Nevertheless, the test results still shed some light on the performance differences between .NET 4 and .NET 4.5 for this scenario:

Task Creation Performance in .NET 4.5

In this post, I will compare the Task creation performance in .NET 4 and .NET 4.5.

I will measure both time and memory consumption associated with Task creation:

public static Tuple<long, long> CreateTasks(int ntasks)
{
    Task[] tasks = new Task[ntasks];
    Stopwatch sw = new Stopwatch();
    Action action = () => { };
    long startBytes = GC.GetTotalMemory(true);
    sw.Start();
    for (int i = 0; i < ntasks; i++) tasks[i] = new Task(action);
    sw.Stop();
    long endBytes = GC.GetTotalMemory(true);
    GC.KeepAlive(tasks);
    return Tuple.Create(sw.ElapsedMilliseconds,endBytes-startBytes);
}

The results on my test machine are as follows:

 

 

The benchmark results do indeed show the smaller footprint of a Task in .NET 4.5, in addition to the decreased amount of time that it takes to create Tasks.

Data Races & Synchronization in .NET 4.5


 

 

 

 

 

 

Data Races

A data race—or race condition—occurs when data is accessed concurrently from multiple threads. Specifically, it happens when one or more threads are writing a piece of data while one or more threads are also reading that piece of data. This problem arises because Windows programs (in C++ and the Microsoft .NET Framework alike) are fundamentally based on the concept of shared memory, where all threads in a process may access data residing in the same virtual address space.

Synchronization
In general, a race condition is when non-deterministic behavior results from threads accessing shared data or resources without following suitable synchronization protocols or locks for serializing threads and to ensure single-threaded access. Accessing shared data from multiple threads concurrently requires that either that shared state be immutable or that the application utilize synchronization to ensure the consistency of the data.

Locking

The following code ensures that the work inside the critical region is executed by at most one thread at a time.

Leaking Locks

The external influences may cause exceptions to occur on a block of code even if that exception is not explicitly stated in the code. This problem is called “Asynchronous Exceptions“. For example, a thread abort may be injected into a thread between any two instructions, though not within a finally block except in extreme conditions. If such an abort occurred after the call to Monitor.Enter but prior to entering the try block, the monitor would never be exited, and the lock would be “leaked.” To help prevent against this, the just-in-time (JIT) compiler ensures that, as long as the call to Monitor.Enter is the instruction immediately before the try block, no asynchronous exception will be able to sneak in between the two. Unfortunately, it is not always the case that these instructions are immediate neighbors. it is often the case that developers want to enter a lock conditionally, such as with a timeout, and in such cases there are typically branching instructions between the call and entering the try block.

Reliable Locking

To address this, in the .NET Framework 4 new overloads of Monitor.Enter and Monitor.TryEnter have been added, supporting a new pattern of reliable lock acquisition and release:

public static void Enter(object obj, ref bool lockTaken);

This overload guarantees that the lockTaken parameter is initialized by the time Enter returns, even in the face of asynchronous exceptions. This leads to the following new, reliable pattern for entering a lock:

Copyright © All Rights Reserved - C# Learners