Understanding Common Problems of Task Programming

TPL is one of the best tool now available in .Net framework to do Parallel Programming which helps developers to create and initiate Tasks in a simple and clean way. Below is the one way to create a simple task.

Task task = new Task(() => Console.WriteLine("My Task")); 
task.Start();

and the other is by using the StartNew method of TaskFactory which creates and starts a new Task.

Task.Factory.StartNew(() => Console.WriteLine("My Task"));

The question is when and why would use one approach versus the other? A Task can start only once, and we need to ensure that multiples calls to taks’s Start method from multiple threads concurrently will only result in the task being scheduled once. This requires synchronization, and synchronization has a cost. if you use Task.Fatory.StartNew, the task will creates and starts at that moment itself and it is no longer possible for threads to race to call Start, also the StartNew method avoids the additional synchronization cost. If you want to know how Microsoft did this,  go here  [takes seconds :)]. Below are the some important points to remember when you deals with Task Programming.

Task Dependency Deadlock

If two or more Tasks depend on each other to complete, none can be move forward without the others, so a deadlock occurs. The only way to avoid this problem is to ensure that your Tasks do not depend on one another. This requires careful attention when writing your Tasks bodies and through testing

Local Variable Evaluation

Assume that you create a series of Tasks in a for loop and refer to the loop counter in your lambda expressions. All of the Tasks end up with the same value because of the way that the C# variable scoping rules are applied to lambda expressions. The simplest way to fix this problem is to pass the loop counter in as a state object to the Task like below.

            BankAccount bankAccount = new BankAccount();
            Task[] tasks = new Task[10];
            for (int i = 0; i < 10; i++)
            {
                tasks[i] = new Task((stateObject) =>
                {
                    int isolatedBalance = (int)stateObject;
                    for (int j = 0; j < 1000; j++)
                    {
                        isolatedBalance++;
                    }
                    return isolatedBalance;
                }, bankAccount.Balance);

                tasks[i].Start();
            }

Excessive Spinning

Many programmers overestimate the performance impact of a Task waiting (either via Thread.Sleep() or by using a CancellationToken wait handle) and use spin waiting instead (through the Thread.SpinWait() method or by entering a code loop). For anything other than exceptionally short waits, spin waiting and code loops will cripple the performance of your parallel program, because avoiding a wait also avoids freeing up a thread for execution

Don’t Synchronize Too Much

Most programmers write the program structure as though they planned for sequential execution and apply parallel features later. Data races arise, which are then fixed using synchronization applied like a Band-Aid. Because the program was designed for sequential use, the programmer finds it hard to synchronize with any granularity and ends up synchronizing everything. synchronization has a cost and can reduce performance. Synchronizing with a heavy hand means that your code will end up running restricting parallelism but still incur the synchronization overheads.

Don’t Synchronize Too Little

A program with too much synchronization is to overcompensate and use too little. Now, we end up with a program that performs well because the number of synchronization points is drastically reduced but doesn’t protect all of the shared data and so suffers from data races.

Pick the Lightest Tool

The .NET Framework includes synchronization primitives from the classic, also some new primitives introduced as part of the TPL. The new primitives are more efficient, but some of them lack useful features available in the heavyweight older primitives. You should pick the most efficient primitive that meets your program requirements.

Don’t Write Your Own Synchronization Primitives

At some point, every parallel programmer struggles to use a particular synchronization primitive in a particular way and thinks, “I should write my own.” You will have those thoughts, too. Don’t do it; don’t write your own. I guarantee you that you will end up making things much worse. Synchronization primitives are very difficult to write correctly. Most of the primitives in the .NET Framework rely on features of the operating system and, in some cases, of the machine hardware. My opinion to you is to change the code whenever you can’t get one of the .NET primitives to fit in with your code.

Advertisement