C# Asynchronous Programming Part3

By | 2015-10-14

In the last two posts we took a look at asynchronous programming in C# using both async/await for asynchronous, but single threaded, programming and using Task.Run as shorthand for Task.Factory.StartNew for multi-threaded programming. Today we are going to briefly look at a few other methods of writing asynchronous code.

Parallel.ForEach
Parallel.ForEach is an easy method to enable parallel processing over an IEnumerable on multiple threads. Parallel.ForEach resides the System.Threading.Tasks namespace and uses lambda expressions to create a delegate in PLINQ(Parallel LINQ). See here for more on Lambda expressions in PLINQ and the TPL. Here is an example of how to the Parallel.ForEach:

List<string> computers = new List<string>()
{
    "computer1",
    "computer2",
    "computer3"
};

Parallel.ForEach(computers, (computer) =>
{
    //PERFORM LONG RUNNING TASKS HERE
});

The above example takes a list of computers and in parallel performs a long running task on each one. The longer the task takes to complete the more benefit you’ll see out of this implementation.

Task.Start
Task.Start schedules a task to run with the TaskScheduler. In essence the task is scheduled on the Global Queue, which will run the task on the next available thread. The Global Queue, maintained by the ThreadPool, operates in a First-In, First-Out sequence. Tasks have several options and can be run synchronously (async by default) and can be waited. Task.Start takes an action delegate and the basic syntax looks like this:

Task t = new Task(() =>
{
    //PERFORM LONG RUNNING TASKS HERE
});
t.Start();
t.Wait();

Note that a task can only be run once and any attempt to start a task a second time will result in an exception.

Task Queueing
Tasks can be queued both globally and locally. What’s the difference? Well, the Global Queue, as mentioned above, operates in a FIFO manner and dequeues tasks onto the next available thread. Top-level tasks get scheduled on the Global Queue. However, a top-level task can have nested or child tasks. Child tasks are scheduled on the Local Queue, which operates in a Last-In, First-Out manner and is specific to the thread that the parent task is running on. Further more, child tasks can have nested or child tasks that they are the parent of. There are lots of options available when nesting tasks and needless to say this discussion can get very complicated very quickly. Here is an abbreviated example:

Task parentTask = Task.Factory.StartNew(() =>
{
    // All tasks below go to the local queue of the parent task.  
    Task nestedTask = new Task(() => 
    {
        /*TASK OPERATION*/
    });  //NESTED TASK

    Task childTask = new Task(() => 
    {
        /*TASK OPERATION*/
    }, TaskCreationOptions.AttachedToParent);  //CHILD TASK

    Task nestingChildTasks = new Task(() =>
    {
        Task nestedChildTask = new Task(() => 
        {
            /*TASK OPERATION*/
        }, TaskCreationOptions.AttachedToParent); 
        nestedChildTask.Start(); //child to nestingChildTasks
    });

    nestedTask.Start();
    childTask.Start();
    nestingChildTasks.Start();
});
parentTask.Wait();

Additional reading on Task Schedulers can be found here.

Lists of Tasks with Task.Factory
Tasks can be started and run together and waited on together. This example builds on the Task.Factory.StartNew method that we briefly looked at in Part2. We will take our list of computers, create a list of tasks(each on it’s own thread), where each Task is a newly started task that will run on one of the computer objects in our list of computers. Lastly we will tell our program to wait until all tasks in the tasklist are finished before moving on.

List<string> computers = new List<string>()
{
    "computer1",
    "computer2",
    "computer3"
};

List<Task> taskList = new List<Task>();
foreach (var computer in computers)
{
    taskList.Add(Task.Factory.StartNew(() => 
    {
        /*LONG RUNNING TASK ON computer OBJECT*/
    }));
}
Task.WaitAll(taskList.ToArray());

Conclusion
As you can see asynchronous programming in C# can be handled many different ways, and this is by no means an exhaustive list. Some key points to remember are that Tasks at the top level will be run on the Global Queue on new threads while child and nested Tasks will be run on the same thread as the parent Task. Regular async/await do not create a new thread, but run on the main thread under a different context until awaited. Parallel.ForEach and Task.Run are both shorthand ways to quickly start background threads either on collections or one at a time.