We are going to create a process for opening Notepad and wait until the Notepad is closed. The program tracks when the Notepad is closed.
Problem statement
Execution of processes in .NET using multithreading concept. Until .NET 4.0 the only possibility was using Thread or BackgroundWorker and handle the processes in different threads and executing them. It needs a lot of code and efforts. From .NET 4.0 Microsoft invested more time in concentrating on the Parallel Programming to leverage multiprocessor architectures. The new parallel programming constructs are:
- Parallel LINQ
- Parallel Class
- Task Parallelism
- Concurrent Collections
- SpinLock and SpinWait.
This article does not discuss all the items above. But we will be concentrating on Task Parallelism and Concurrent Collections today.
Task Parallelism is the lowest level approach to parallelization. The classes that comes under Task Parallelism are grouped in the namespace System.Threading.Tasks and this namespace contains the following:
Class |
Purpose |
Task |
For managing unit of work |
Task<TResult> |
For managing a unit of work with a return value |
TaskFactory |
For creating tasks |
TaskFactory<TResult> |
For creating tasks and continuations with the same type |
TaskCompletionSource |
For manually controlling a task's workflow |
Everyone should have studied the Producer-Consumer algorithm. If you are interested in learning more about that you can refer to the following article:
http://blogs.msdn.com/b/csharpfaq/archive/2010/08/12/blocking-collection-and-the-producer-consumer-problem.aspx
In this article the author beautifully explains the BlockingCollection<T> class and how it solves the Producer-Consumer problem. In our example, we are going to use the same class.
Before going to the actual program, learn the following terms used in our programming:
- Task.Factory.StartNew - This starts a new task for each consumer.
- Collection.CompleteAdding() - This indicates that the collection has completed adding and it will not allow any more addition of items and attempts to remove from the collection will not wait when the collection is empty.
- Task.WaitAll - You can wait on multiple tasks at once - via the static methods Task.WaitAll (Wait for the specific tasks to finish).
- Collection.TryTake - Attempts to remove an item from the BlockingCollection<T>.
- p.Start() - Starts a process and associates it with a System.Process component.
- p.WaitForExit() - Tells the Process component to wait for indefinitely for the associated process to exit.
- Collection.IsAddingCompleted - Whether this collection has been marked as complete for adding.
For the example, I am taking only 5 instances of Notepad.exe processes that are spawn concurrently and their exit is tracked and displayed in a console application. This is a simple example, but a good one to understand the Parallel Programming introduced in .NET.
Here is my code:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Threading.Tasks;
class Program
{
static readonly BlockingCollection<string> Collection = new BlockingCollection<string>();
private static int _count = 0;
static void Main()
{
const int maxTasks = 5;//we are going to spawn 5 processes for our example.
var tasks = new List<Task> {
Task.Factory.StartNew(() => {
for(var i = 0; i < 5; i++)
{
Collection.Add(i.ToString(CultureInfo.InvariantCulture)); //adding 5 items to the collection object.
}
Console.WriteLine("Spawning multiple processes completed.
Now wait and see until all the jobs are completed.");
Collection.CompleteAdding();
}),
};
for (var i = 0; i < maxTasks; i++)
{
tasks.Add(Task.Factory.StartNew(UserTasks(i)));//Add new tasks
}
Task.WaitAll(tasks.ToArray()); // wait for completion
}
static Action UserTasks(int id)
{
// return a closure just so the id can get passed
return () =>
{
while (true)
{
string item;
if (Collection.TryTake(out item, -1))
{
using (Process p = new Process())
{
p.StartInfo.FileName = "notepad.exe";
//p.StartInfo.Arguments = item;
p.Start();
p.WaitForExit();
var exitCode = p.ExitCode;
Console.WriteLine(exitCode == 0 ? "{0} exited successfully!!!" : "{0} exited failed!!!", p.Id);
}
}
else if (Collection.IsAddingCompleted)
{
break; // exit loop
}
}
Console.WriteLine("Consumer {0} finished", id);
_count = _count + 1;
if (_count == 4)
{
Console.ReadLine();
}
};
}
}
In this program:
- We have initially created an instance of the BlockingCollection<String> object.
- We have created a new Task list and added new tasks using Task.Factory.StartNew.
- Then we have added the string objects to the BlockingCollection object.
- Once the string objects have been added we set the completion by CompletionAddding() method.
- We have written an Action (Functional Programming) called UserTasks and added code for spawning a process.
Now let us run the program. Click on F5. You will see that it has opened 5 Notepads in your application and you will see a message that the spawning of multiple processes has completed.
Now close one of the opened Notepads and you will see the following message in the console:
Now close each of the Notepad windows one by one and observe the console. When you have closed all the Notepad windows you should see the final output as below:
Hope you liked this simple program. Instead of Notepads, in real-world projects we may need to spawn multiple jobs or tasks that need to be running asynchronously and concurrently and always reporting the status back to the main thread. You can extend this application from simple to complex tasks.
References: MSDN site and Threading in C# by Albahari.
Threading in C# is an excellent work by Albahari that would give you more insight into the Multi-Threading support in .NET and I would recommend every programmer either to learn or revise the concepts of threading.