The content and code of this article is referenced from book Pro C#5.0 and the .NET 4.5 Framework by Apress. The intention of the writing is to review the konwledge and gain better understanding of the .net framework.
1. Process/AppDomain/Context/Thread relationship
A thread was defined as a path of execution within an executable applicaiton. By creating additional threads, you can build more responsive but not necessarily faster applications.
The System.Threading namespace was released with .net 1.0.
static void ExecutingThread() { Thread currThread = Thread.CurrentThread; //get current executing thread }
In .net framework, there is not a direct one-to-one correspondence between application domains and threads. However, a given thread can execute within only a single application domain at any point in time.
static void ExecutingThread() { Thread currThread = Thread.CurrentThread; //get current executing thread AppDomain domain = Thread.GetDomain(); //obtain hosting appdomain }
1.1 The problem of concurrency
One of the many pains of multithreaded programming is that you have little control over how the underlying operating system or the CLR makes use of threads.
Furthermore, given that threads can be moved between application and contextual boundries as required by the CLR, you must be mindful of which aspects of your application are thread-volatile, and which operations are atomic.
1.2 Thread synchronization
It should be clear that multithreaded programs are in themselves quite volatile, as numerous threads can operate on the shared resources at the same time. To protect the resource from possible corruption, .net developer must make use of threading primitives (such as lock, monitors, and [synchronization]] to control the access among executing threads.
Using types defined within the System.Threading namespace, the .net 4.0 and higher Task Parallel Library (TPL), and the .net 4.5 C# async and await language keywords, you are able to work with multiple threads with minimal fuss.
2. A brief review of the .net delegate
Recall that .net delegate is essentially a type-safe, object-oriented, function pointer. When you define a delegate type, the C# compiler responds by building a sealed class that derives from System.MulticastDelegate.
public delegate int BinaryOp (int x, int y); //generated class public sealed class BinaryOp : System.MulticastDelegate { public BinaryOp (object target, uint functionAddress); public int Invoke (int x, int y); public IAsyncResult BeginInvoke(int x, int y, AsyncCallback cb, object state); public int EndInvoke (IAsyncResult result); }
Recall that the generated Invoke() method is used to invoke the methods maintained by a delegate object in synchronous manner. Therefore, the calling thread is forced to wait until the delegate invocation completes.
2.1 the asynchronous nature of delegates
When C# compiler processes the delegate keyword, the dynamically generated class defines two methods named BeginInvoke() and EndInvoke().
The BeginInvoke() method always returns an object implementing the IAsyncResult interface, while EndInvoke() requires an IAsyncResult-compatible type as its sole parameter.
public interface IAsyncResult { object AsyncState{get;} WaitHandle AsyncWaitHandle{get;} bool CompletedSynchronously{ get; } bool IsCompleted { get; } }
In the simplest case, you are able to avoid directly invoking these members. All you have to do is cache the IAsyncResult-compatible object returned by BeginInvoke() and pass it to EndInvoke().
2.2 Invoking a method Asynchronously
public static void Main (string[] args) { Console.WriteLine ("Thread id {0} ", Thread.CurrentThread.ManagedThreadId); BinaryOp b = new BinaryOp (Add); IAsyncResult result = b.BeginInvoke (10, 10, null, null); int answer = b.EndInvoke (result); Console.WriteLine ("answer is {0}", answer); } public static int Add(int x , int y) { Console.WriteLine ("Thread id {0}", Thread.CurrentThread.ManagedThreadId); Thread.Sleep(5000); return x + y; }
2.3 Synchronizing the calling thread
Obviously, asynchronous delegates would lose their appeal if the calling thread had the potential of being blocked under various circumstances. To allow the calling thread to discover whether the asynchronous invoked method has completed, the IAsyncResult provides the IsCompleted property.
public static void Main (string[] args) { Console.WriteLine ("Thread id {0} ", Thread.CurrentThread.ManagedThreadId); BinaryOp b = new BinaryOp (Add); IAsyncResult result = b.BeginInvoke (10, 10, null, null); while (!result.IsCompleted) { Console.WriteLine ("doing work"); Thread.Sleep (1000); } int answer = b.EndInvoke (result); Console.WriteLine ("answer is {0}", answer); }
In addition to the IsCompleted property, the IAsyncResult interface provides the AsyncWaitHandle property for more flexible waiting logic.
while (!result.AsyncWaitHandle.WaitOne(1000, ture)) { Console.WriteLine ("doing work"); }
2.4 The role of AsyncCallback delegate
Rather than polling a delegate to determine whether an asynchronously invoked method has completed, it would be more efficient to have a secondary thread inform the calling thread when the task is finished. In this case, you need to supply an instance of the System.AsyncCallback delegate as a parameter to BeginInvoke().
3. System.Threading
The System.Threading namespace provides a number of types than enable the direct construction of multithreaded applications.
3.1 System.Threading.Thread
This class represents an object-oriented wrapper around a given path of execution within a particular AppDomain. It also defines a number of methods that allow you to create new threads, as well we suspend, stop and destory.
Static Methods | Meaning |
CurrentContext | This read-only property returns the context in which the thread is currently running |
CurrentThread | This read-only property returns a reference to the currently running thread |
GetDomain() | This method return a referenc to the current AppDomain |
GetDomainID() | current AppDomain ID |
Sleep | This method suspends the current thread for a specific time |
public static void Main (string[] args) { Thread primaryThread = Thread.CurrentThread; primaryThread.Name = "ThePrimaryThread"; // Console.WriteLine("Current Domain is {0}", Thread.GetDomain().FriendlyName); Console.WriteLine ("Current context is {0}", Thread.CurrentContext.ContextID); Console.WriteLine ("Thread name is {0}", primaryThread.Name); Console.WriteLine ("Thread is alive {0}", primaryThread.IsAlive); Console.WriteLine ("Priority is {0}", primaryThread.Priority); Console.WriteLine ("Thread state is {0}", primaryThread.ThreadState); }
Do notice that the Thread class supports a property called Name. If you do not set this value, Name will return an empty string.
3.2 Manually create secondary threads
When you want to progammatically create additional threads to carry on some work, follow this process.
(1) Create a method to be the entry point for the new thread
(2) Create a new ParameterizedThreadStart (or ThreadStar) delegate, passing the address of method defined in step1
(3) Create a thread object, passing the ParameterizedThreadStart/ThreadStart delegate as a constructor as constructor argument
(4) Establish any initial thread characteristics
(5) Call Thread.Start() method.
The ThreadStart delegate can point to any method that takes no arguments and return nothing. And the ParameterizedThreadStart delegate allows a single parameter of type system.Object.
3.3 Working with ThreadStart delegate
static void Main(string[] args) { //name the current thread Thread primaryThread = Thread.CurrentThread; primaryThread.Name = "Primary"; Console.WriteLine("{0} is executing Main()", Thread.CurrentThread.Name); Printer p = new Printer(); //Create second thread Thread backgroundThread = new Thread(new ThreadStart(p.PrintNumber)) {Name = "Secondary"}; backgroundThread.Start(); Console.ReadLine(); }
3.4 Working with the ParameterizedThreadStart Delegate
static void Main(string[] args) { Console.WriteLine("ID of thread is Main() : {0}", Thread.CurrentThread.ManagedThreadId); //pass object to secondary thread AddParam ap = new AddParam(10, 10); Thread t = new Thread(new ParameterizedThreadStart(Add)); t.Start(ap); Thread.Sleep(5); Console.ReadLine(); } private static void Add(object data) { if (data is AddParam) { Console.WriteLine("Thread in Add() is {0}", Thread.CurrentThread.ManagedThreadId); AddParam ap = data as AddParam; Console.WriteLine("{0} + {1} is {2} ", ap.a, ap.b, ap.a + ap.b); } }
3.5 AutoResetEvent class
One simple, and thread-safe way to force a thread to wait until another is completed is to use the AutoResetEvent class.
private static AutoResetEvent waitHandle = new AutoResetEvent(false); static void Main(string[] args) { Console.WriteLine("ID of thread is Main() : {0}", Thread.CurrentThread.ManagedThreadId); //pass object to secondary thread AddParam ap = new AddParam(10, 10); Thread t = new Thread(new ParameterizedThreadStart(Add)); t.Start(ap); //wait here until you are notified waitHandle.WaitOne(); Console.ReadLine(); } private static void Add(object data) { if (data is AddParam) { Console.WriteLine("Thread in Add() is {0}", Thread.CurrentThread.ManagedThreadId); AddParam ap = data as AddParam; Console.WriteLine("{0} + {1} is {2} ", ap.a, ap.b, ap.a + ap.b); //tell other thread we are done waitHandle.Set(); } }
3.6 Foreground and Background thread
Foreground threads have the ability to prevent the current application from terminating. The CLR will not shut down an application until all foreground threads have ended.
Background threads are viewed by the CLR as expendable paths of execution that can be ignored.
By default, every thread you create via the Thread.Start() method is automatically a foreground threads. But you are free to configure it as background via thread.IsBackgruond = true;
4. The issue of Concurrency
When you build multithreaded applicaions, your program needs to ensure that any picec of shared data is protected against the possibility of numerous threads changing its value.
4.1 using C# lock keyword
The first technique you can use to synchronize access to shared resource is the C# lock keyword. This keyword allos you to define a scope of statements that must be synchronized between threads. The lock keyword requires you to specify a token that must be acquired by a thread to enter within the lock scope.
It is safer and best practise to declare a private object variable to serve as the lock.
private object threadLock = new object(); public void PrintNumber() { lock (threadLock) { //display current thread Console.WriteLine("Current thread is {0}", Thread.CurrentThread.ManagedThreadId); for (int i = 0; i < 10; i++) { Random r = new Random(); Thread.Sleep(1000* r.Next(5)); Console.WriteLine("{0}, ", i); } Console.WriteLine(); } }
4.2 system.Threading.Monitor
The C# lock statement is really just a shorthand notation for working with the System.Threading.Monitor class.
public void PrintNumber() { Monitor.Enter(threadLock); try { //display current thread Console.WriteLine("Current thread is {0}", Thread.CurrentThread.ManagedThreadId); for (int i = 0; i < 10; i++) { Random r = new Random(); Thread.Sleep(1000*r.Next(5)); Console.WriteLine("{0}, ", i); } Console.WriteLine(); } finally { Monitor.Exit(threadLock); } }
4.3 [Synchronization] attribute
[Synchronization] attribute is a member of Sytem.Runtime.Remoting.Contexts. This class level attribute effectively locks down all instance member code of the object for thread safety. When the CLR allocates objects attributed with [Synchronization], it will place the object within a synchronized context. Objects that should not be removed from a contextual boundary should derive from ContextBoundObject.
[Synchronization] public class Printer : ContextBoundObject { }
The CLR will still lock invocations to the method, event if the method is not making use of thread-sensitive data.
5. Programming with Timer callbacks
public static void Main (string[] args) { Console.WriteLine ("Main() thread is {0}", Thread.CurrentThread.ManagedThreadId); TimerCallback timeCb = new TimerCallback (PrintTime); Timer timer = new Timer (timeCb, "hello", 0, 1000); //execute per second Console.WriteLine ("Press any key to terminate"); Console.ReadLine (); } private static void PrintTime(object o) { Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); }
6. Understanding CLR ThreadPool
When you invoke a method asynchronously using delegate types, the CLR does not create a brand new thread. Instead, it fetches a thread from thread pool maintained by the runtime.
If you would like to queue a method call for processing by a worker thread in the pool, you can make use of ThreadPool.QueueUserWorkItem() method.
public static void Main (string[] args) { Console.WriteLine ("Main thread started, thread id is {0}", Thread.CurrentThread.ManagedThreadId); WaitCallback callback = new WaitCallback (PrintCar); Car car = new Car (){ CarName = "BMW" }; ThreadPool.QueueUserWorkItem (callback, car); Console.ReadLine (); } private static void PrintCar(object state) { Console.WriteLine ("Work thread id is {0}", Thread.CurrentThread.ManagedThreadId); if (state is Car) { Car myCar = state as Car; Console.WriteLine (myCar.CarName); } }
7. Parallel programming using Task Parallel Library
Begin with .net 4.0, Microsoft introduced a new approach to multithreaded application development using Task Parallel Library. Using the types of System.Threading.Tasks, you can build scalable parallel code without working directly with thread or threadpool.
7.1 System.Threading.Task namespace
The CPL will automatically distribute your application‘s workload across available CPUs dynamically, using the CLR thread pool. The end result is that you can maximize the performance of your .net applications, while being shielded from many of complexities of directly working with threads.
7.2 The role of the parallel class
A key class of the TPL is System.Threading.Tasks.Parallel. This class supports a number of methods that allow you to iterate over a collection of data (IEnumerable<T>) in a parallel fashion. There are two primary static members, Parallel.For() and Parallel.ForEach(), each of which defines numerous overloaded versions.
These methods are the same sort of logic you would write in a normal looping construct. The benefit is that the Parallel class will pluck threads from the thread pool on your behalf.
In addition, you will need to make use of System.Func<T> and System.Action<T> delegates to specify the target method that will be called to process the data.
7.3 Data parallelism with the parallel class
The first way to use the TPL is to perform data parallelism. This term refers to the task of iterating over an array or collection in a parallel manner using Parallel.For() or Parallel.ForEach() methods.
First, we look at the example of processing files in a blocking manner.
private static void ProcessFiles() { string[] files = Directory.GetFiles(@"D:\files", "*.jpg", SearchOption.AllDirectories); string newDir = @"D:\ModifiedPics"; Directory.CreateDirectory(newDir); //process all images in main thread foreach (string file in files) { string fileName = Path.GetFileName(file); using (Bitmap bitmap = new Bitmap(file)) { bitmap.RotateFlip(RotateFlipType.Rotate180FlipNone); bitmap.Save(Path.Combine(newDir, fileName)); } } }
Here is the Parallel.ForEeach() implementation
private static void ProcessFiles() { string[] files = Directory.GetFiles(@"D:\files", "*.jpg", SearchOption.AllDirectories); Parallel.ForEach(files, (file) => { string fileName = Path.GetFileName(file); using (Bitmap bitmap = new Bitmap(file)) { bitmap.RotateFlip(RotateFlipType.Rotate180FlipNone); bitmap.Save(Path.Combine(@"D:\Pics", fileName)); } }); }
7.4 Accessing UI element on Secondary Threads
GUI controls have thread affinity with the thread that created it. If secondary threads attempt to access a control it did not directly create, you are bound to run into runtime errors.
One approach that we can use to allow these secondary threads to access the controls in a thread-safe manner is to make use of yet another delegate centric technique, anonymous delegate. The Control parent class of the Windows Form API defines a method named Invoke(), which takes a System.Delegate as input. You can call this method to provide a thread-safe manner of update the UI of the given control.
this.Invoke((Action) delegate{
this.Text = "testing"; //update UI
});
7.5 The Task class
The Task class allows you to easily invoke a method on a secondary thread, and can be used as a simple alternative to working with asynchronous delegate.
Task.Factory.StartNew(() => { //processing });
The factory property of Task returns a TaskFactory object.
7.6 Handle Cancellatino Request
Parallel.ForEach() and Parallel.For() methods both support cancellation through the use of cancellation tokens. When you invoke methods on Parallel, you can pass in a ParallelOptions object, which in turn contains a CancellationTokenSource object.
private CancellationTokenSource cancelToken = new CancellationTokenSource();
You can call cancelToken.Cancel() to stop the thread.
private static void ProcessFiles() { ParallelOptions parOpts = new ParallelOptions(); parOpts.CancellationToken = cancelToken.Token; parOpts.MaxDegreeOfParallelism = System.Environment.ProcessorCount; try { Parallel.ForEach(files, parOpts, currentfile => { parOpts.CancellationToken.ThrowIfCancellationRequested(); //processing }); } catch (OperationCanceledException ex) { throw; } }
7.7 Task Parallelism using the Parallel class
In addition to data parallelism, TPL can also be used to fire off any number of asynchronous tasks using the Parallel.Invoke() method.
Parallel.Invoke(
() => {}, ()=>{}
);
The parallel.Invoke() method expects a parameter array of Action<> delegate.
7.8 Parallel Linq queries (PLINQ)
If you choose, you can make use of a set of extension methods, which allows you to construct a LINQ query to perform its workload in parallel.
private static void ProcessNumbers() { int[] source = Enumerable.Range(1, 100000).ToArray(); int[] subset = (from num in source.AsParallel() where num%3 == 0 orderby num select num).ToArray(); }
int[] subset = (from num in source.AsParallel().WithCancellation(cancelToken.Token) where num%3 == 0 orderby num select num).ToArray();
5. Asynchronous calls .net 4.5
With the release of .net 4.5, the C# programming language has been updated with two keywords that further simplify the process of authoring asynchronous code. When you make use of new async and await keywords,the compiler will genearte threading code on your behalf .
The async keyword is used to qualify that a method should be called in an asynchronous manner automatically. Simply by marking a method with async, the CLR will create a new thread of execution to handle the task at hand. And when you are calling an async method, the await keyword will automatically pause the current thread from any further activity until the task is complete, leaving the calling thread free to continue on its way.
private async void btnCallMethod_Click(object sender, EventArgs e) { txtInput.Text = await DoWork(); } private Task<string> DoWork() { return Task.Run(() => { Thread.Sleep(10000); return "done"; }); }
5.1 Async method with multiple await
It is permissible for a single async method to have multiple await contexts within its implementation.