Closures in the Task Parallel Library, what are they? …beware of race conditions.
Up to .NET 3.5 multi-threading programming had it challenges. Multi-threading is a way to improve application performance and responsiveness by running long operations in a different thread from the main application thread.
Up to .NET 3.5 the parallel threads created by an application domain only targeted a single CPU core by CPU affinity. We also know that CPUs (Central Processing Units) couldn’t increase the processor clock without melting the integrated circuits away and requiring bigger cooling fans. So, the hardware evolved into placing several CPU cores in parallel to increase computing power.
The task parallel library introduced in .NET 4.0 responded to the need of catching up with the hardware capabilities and as a way to execute parallel operations in different cores.
Each task created by the TPL has its own stack and a thread or set of CPU threads.
When creating a task, a common code snipped to launch a task using a Lambda expression as an Action is:
1. Task.Factory.StartNew( () => 2. { 3. statement1; 4. statement2; 5. } 6. ); |
However, very rarely a newly created task will receive no data from the method invoking it.
It is more likely to have the following code inside a C# method calling a parallel task: 1. … 2. int y=45; 3. 4. Task.Factory.StartNew( () => 5. { 6. y++; 7. } 8. ); 9. … |
y is a variable that is passed to the parallel task for further processing.
Now, what would be the value of the variable y when the task finishes running?
Is y passed by value or by reference?
After the task completes the value of the primitive y is 46. y is indeed passed by reference. These variables passed to a task receive the name of closures.
Now, from multi-threading programming you might remember that objects that were shared between several threads could end up with a value that was not predictable, this was the dreaded race condition. There were ways to mitigate this condition such as using the lock statement to avoid concurrent threads affect the state of the object in an unpredictable way. Threads compete for CPU time and there is no guarantee that they will execute sequentially or in a predictable order.
You can also cause race conditions with closures in the task parallel library. Let’s see how:
1. … 2. int y=45;</code> 3. 4. Task.Factory.StartNew( () => 5. { 6. y++; 7. } 8. ); 9. … 10. Console.WriteLine(y); |
In the example above, y won’t always be 46.
Once the task is kicked off, it is set in queue for processor time, and so is the main thread where the method that starts the task is.
They both compete in parallel for CPU cycles and run as parallel as possible. The executing code gets forked. If the main thread runs first while the parallel task is waiting in queue, the value of y will be displayed on the console as 45. If the task runs before the main thread makes it to the Console.WriteLine statement, the displayed value will be 46.
This is a typical race condition.
You have several ways to mitigate this condition in the TPL. One of them is shown below:
1. int y=45; 2. Task t = null; 3. t = Task.Factory.StartNew( () => 4. { 5. y++; 6. } 7. ); 8. Task.WaitAll(t); 9. Console.WriteLine(y); |
The example above will always show 46 in the console.
Best Microsoft MCTS Certification, Microsoft MCITP Training at certkingdom.com