|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
IntroductionI'm afraid to say that I am just one of those people, that unless I am doing something, I am bored. So now that I finally feel I have learnt tha basics of WPF, it is time to turn my attention to other matters. I have a long list of things that demand my attention such as (WCF/WF/CLR Via C# version 2 book), but I recently went for a new job (and got, but turned it down in the end) which required me to know a lot about threading. Whilst I consider myself to be pretty good with threading, I thought yeah I'm ok at threading, but I could always be better. So as a result of that I have decided to dedicate myself to writing a series of articles on threading in .NET. This series will undoubtely owe much to an excellent Visual Basic .NET Threading Handbook that I bought that is nicely filling the MSDN gaps for me and now you. I suspect this topic will range from simple to medium to advanced, and it will cover a lot of stuff that will be in MSDN, but I hope to give it my own spin also. So please forgive me if it does come across a bit MSDN like. I dont know the exact schedule, but it may end up being something like
I guess the best way is to just crack on. One note though before we start, I will be using C# and Visual Studio 2008. What I'm going to attempt to cover in this article will be This article will be all about how to control the synchronization of different threads. Wait HandlesOne of the first steps one must go through when trying to understand how to get multiple threads to work together well, is how to sequence operations. For example lets say we have the following problem
Now these may look like very simply tasks, that don't need to be threaded at all, but for the sake of this demo let's assume that each of these steps is a rather long operation involving many calls to a fictional database. We can see from the brief above, that we can't do step2 until step1 is done, and we can't do step3 until step2 is done. This is a dilemma. We could of course have all this in one monolithic code chunk, but this defeats the idea of concurrency that we are trying to use to make our application more responsive (remember each of these fictional steps takes a long time). So what can we do, we know we wan't to thread these 3 steps but there is certainly some issues, as we can NOT garuentee which thread will be started first, as we saw in Part2. Mmm this isn't good. Luckily help is at hand. There is an idea within .NET of a The way that I like to think about the gerenal idea behind
That's how I think about it any way. So what does .NET offer in the way of
The following classes are provided for us
As can be seen in this heirachy, Some important common (but not all) methods are as follows SignalAndWaitThere are several overloads for this method, but the basic idea is that one
WaitAll (Static method on WaitHandle)There are several overloads for this method, but the basic idea is that an
array of WaitAny (Static method on WaitHandle)There are several overloads for this method, but the basic idea is that an
array of WaitOneThere are several overloads for this method, but the basic idea is the current Let us now concentrate on EventWaitHandleThe So let's now look a little bit further at the AutoResetEventFrom MSDN "A thread waits for a signal by calling Calling Set signals In lamens terms when using a Let's consider a small example where 2 threads are started. The 1st thread
will run for a period of time and will then signal (calling Set) a Here is some code that illustrates this. using System;
using System.Threading;
namespace AutoResetEventTest
{
class Program
{
public static Thread T1;
public static Thread T2;
//This AutoResetEvent starts out non-signalled
public static AutoResetEvent ar1 = new AutoResetEvent(false);
//This AutoResetEvent starts out signalled
public static AutoResetEvent ar2 = new AutoResetEvent(true);
static void Main(string[] args)
{
T1 = new Thread((ThreadStart)delegate
{
Console.WriteLine(
"T1 is simulating some work by sleeping for 5 secs");
//calling sleep to simulate some work
Thread.Sleep(5000);
Console.WriteLine(
"T1 is just about to set AutoResetEvent ar1");
//alert waiting thread(s)
ar1.Set();
});
T2 = new Thread((ThreadStart)delegate
{
//wait for AutoResetEvent ar1, this will wait for ar1 to be signalled
//from some other thread
Console.WriteLine(
"T2 starting to wait for AutoResetEvent ar1, at time {0}",
DateTime.Now.ToLongTimeString());
ar1.WaitOne();
Console.WriteLine(
"T2 finished waiting for AutoResetEvent ar1, at time {0}",
DateTime.Now.ToLongTimeString());
//wait for AutoResetEvent ar2, this will skip straight through
//as AutoResetEvent ar2 started out in the signalled state
Console.WriteLine(
"T2 starting to wait for AutoResetEvent ar2, at time {0}",
DateTime.Now.ToLongTimeString());
ar2.WaitOne();
Console.WriteLine(
"T2 finished waiting for AutoResetEvent ar2, at time {0}",
DateTime.Now.ToLongTimeString());
});
T1.Name = "T1";
T2.Name = "T2";
T1.Start();
T2.Start();
Console.ReadLine();
}
}
}
This results in the following output, where it can be seen that the T1 waits
for 5 secs (simulated work) and T2 waits for the
ManualResetEventFrom MSDN "When a thread begins an activity that must complete before other
threads proceed, it calls Reset to put Once it has been signaled, In lamens terms when using a Consider the following code snippet using System;
using System.Threading;
namespace ManualResetEventTest
{
/// <summary>
/// This simple class demonstrates the usage of an ManualResetEvent
/// in 2 different scenarios, bith in the non-signalled state and the
/// signalled state
/// </summary>
class Program
{
public static Thread T1;
public static Thread T2;
public static Thread T3;
//This ManualResetEvent starts out non-signalled
public static ManualResetEvent mr1 = new ManualResetEvent(false);
static void Main(string[] args)
{
T1 = new Thread((ThreadStart)delegate
{
Console.WriteLine(
"T1 is simulating some work by sleeping for 5 secs");
//calling sleep to simulate some work
Thread.Sleep(5000);
Console.WriteLine(
"T1 is just about to set ManualResetEvent ar1");
//alert waiting thread(s)
mr1.Set();
});
T2 = new Thread((ThreadStart)delegate
{
//wait for ManualResetEvent mr1, this will wait for ar1
//to be signalled from some other thread
Console.WriteLine(
"T2 starting to wait for ManualResetEvent mr1, at time {0}",
DateTime.Now.ToLongTimeString());
mr1.WaitOne();
Console.WriteLine(
"T2 finished waiting for ManualResetEvent mr1, at time {0}",
DateTime.Now.ToLongTimeString());
});
T3 = new Thread((ThreadStart)delegate
{
//wait for ManualResetEvent mr1, this will wait for ar1
//to be signalled from some other thread
Console.WriteLine(
"T3 starting to wait for ManualResetEvent mr1, at time {0}",
DateTime.Now.ToLongTimeString());
mr1.WaitOne();
Console.WriteLine(
"T3 finished waiting for ManualResetEvent mr1, at time {0}",
DateTime.Now.ToLongTimeString());
});
T1.Name = "T1";
T2.Name = "T2";
T3.Name = "T3";
T1.Start();
T2.Start();
T3.Start();
Console.ReadLine();
}
}
}
Which results in the following screen shot
It can be seen that 3 threads (T1-T3) are started T2 and T3 are both waiting
on the Back To The Original ProblemRecall the orginal problem
This should now be pretty easy to solve. All we need are some using System;
using System;
using System.Threading;
namespace OrderSystem
{
/// <summary>
/// This simple class demonstrates the usage of an AutoResetEvent
/// to create some synchronized threads that will carry out the
/// following
/// -CreateOrder
/// -SaveOrder
/// -PrintOrder
///
/// Where it is assumed that these 3 task MUST be executed in this
/// order, and are interdependant
/// </summary>
public class Program
{
public static Thread CreateOrderThread;
public static Thread SaveOrderThread;
public static Thread PrintOrderThread;
//This AutoResetEvent starts out non-signalled
public static AutoResetEvent ar1 = new AutoResetEvent(false);
public static AutoResetEvent ar2 = new AutoResetEvent(false);
static void Main(string[] args)
{
CreateOrderThread = new Thread((ThreadStart)delegate
{
Console.WriteLine(
"CreateOrderThread is creating the Order");
//calling sleep to simulate some work
Thread.Sleep(5000);
//alert waiting thread(s)
ar1.Set();
});
SaveOrderThread = new Thread((ThreadStart)delegate
{
//wait for AutoResetEvent ar1, this will wait for ar1
//to be signalled from some other thread
ar1.WaitOne();
Console.WriteLine(
"SaveOrderThread is saving the Order");
//calling sleep to simulate some work
Thread.Sleep(5000);
//alert waiting thread(s)
ar2.Set();
});
PrintOrderThread = new Thread((ThreadStart)delegate
{
//wait for AutoResetEvent ar1, this will wait for ar1
//to be signalled from some other thread
ar2.WaitOne();
Console.WriteLine(
"PrintOrderThread is printing the Order");
//calling sleep to simulate some work
Thread.Sleep(5000);
});
CreateOrderThread.Name = "CreateOrderThread";
SaveOrderThread.Name = "SaveOrderThread";
PrintOrderThread.Name = "PrintOrderThread";
CreateOrderThread.Start();
SaveOrderThread.Start();
PrintOrderThread.Start();
Console.ReadLine();
}
}
}
Which produces the following screen shot
SemaphoresA Semaphore inherits from
I read something that described a Let's see a simple example, where the using System;
using System;
using System.Threading;
namespace SemaphoreTest
{
class Program
{
//initial count to be satified concurrently = 2
//maximum capacity = 5
static Semaphore sem = new Semaphore(2, 5);
static void Main(string[] args)
{
for (int i = 0; i < 10; i++)
{
new Thread(RunThread).Start("T" + i);
}
Console.ReadLine();
}
static void RunThread(object threadID)
{
while (true)
{
Console.WriteLine(string.Format(
"thread {0} is waiting on Semaphore",
threadID));
sem.WaitOne();
try
{
Console.WriteLine(string.Format(
"thread {0} is in the Semaphore, and is now Sleeping",
threadID));
Thread.Sleep(100);
Console.WriteLine(string.Format(
"thread {0} is releasing Semaphore",
threadID));
}
finally
{
//Allow another into the Semaphore
sem.Release();
}
}
}
}
}
Which results in something similar to this This example does show a working example of how a using System;
using System.Threading;
using System.Data;
using System.Data.SqlClient;
namespace SemaphoreTest
{
/// <summary>
/// This example shows partially completed skeleton
/// code for consuming a limited resource, such as a
/// DB connection using a Semaphore
///
/// NOTE : THIS CODE WILL NOT RUN, ITS INCOMPLETE
/// DEMO ONLY CODE
/// </summary>
class RestrictedDBConnectionStringAccessUsingSemaphores
{
//initial count to be satified concurrently = 1
//maximum capacity = 3
static Semaphore sem = new Semaphore(1, 3);
static void Main(string[] args)
{
//start 5 new threads that all require a Database connection
//but as a DB connection is limited to 3, we use a Semaphore
//to ensure that the number of active connections will never
//exceed the total allowable DB connections
new Thread(RunCustomersThread).Start("ReadCustomersFromDB");
new Thread(RunOrdersThread).Start("ReadOrdersFromDB");
new Thread(RunProductsThread).Start("ReadProductsFromDB");
new Thread(RunSuppliersThread).Start("ReadSuppliersFromDB");
Console.ReadLine();
}
static void RunCustomersThread(object threadID)
{
//wait for the Semaphore
sem.WaitOne();
//the MAX DB connections must be within its limited
//so proceed to use the DB
using (new SqlConnection("<SOME_DB_CONNECT_STRING>"))
{
//do our business with the database
}
//Done with DB, so release Semaphore which will
//allow another into the Semaphore
sem.Release();
}
static void RunOrdersThread(object threadID)
{
//wait for the Semaphore
sem.WaitOne();
//the MAX DB connections must be within its limited
//so proceed to use the DB
using (new SqlConnection("<SOME_DB_CONNECT_STRING>"))
{
//do our business with the database
}
//Done with DB, so release Semaphore which will
//allow another into the Semaphore
sem.Release();
}
static void RunProductsThread(object threadID)
{
//wait for the Semaphore
sem.WaitOne();
//the MAX DB connections must be within its limited
//so proceed to use the DB
using (new SqlConnection("<SOME_DB_CONNECT_STRING>"))
{
//do our business with the database
}
//Done with DB, so release Semaphore which will
//allow another into the Semaphore
sem.Release();
}
static void RunSuppliersThread(object threadID)
{
//wait for the Semaphore
sem.WaitOne();
//the MAX DB connections must be within its limited
//so proceed to use the DB
using (new SqlConnection("<SOME_DB_CONNECT_STRING>"))
{
//do our business with the database
}
//Done with DB, so release Semaphore which will
//allow another into the Semaphore
sem.Release();
}
}
}
From this small example it should be clear that the MutexA Sasha Goldshtein, the technical reviewer of this article, also stated that
when using a Application Single InstanceOne of the most common uses of a Let's see some code. This code ensures a single instance of an application. Any new instance will wait 5 seconds (in case the currently running instance is in the process of closing) before assuming there is already a previous instance of the application running, and exiting. using System;
using System.Threading;
namespace MutexTest
{
class Program
{
//start out with un-owned mutex
static Mutex mutex = new Mutex(false,"MutexTest");
static void Main(string[] args)
{
//check to see if there is another instance, allow 5 secs
//another instance may be in process of closing right now
if(!mutex.WaitOne(TimeSpan.FromSeconds(5)))
{
Console.WriteLine("MutexTest already running! Exiting");
return;
}
try
{
Console.WriteLine("MutexTest Started");
Console.ReadLine();
}
finally
{
//release the mutx to allow, possible future instance to run
mutex.ReleaseMutex();
}
}
}
}
So if we start 1 instance of this app, we get
And then we try and run another copy, we then get the following (after a 5 second delay)
Critical Sections AKA LocksLocking is a mechanism for ensuring that only 1 thread can enter a particular section of code at a time. This is done by the use of what are known as locks. The actual locked sections of code are known as critical sections. There are various different ways in which to lock a section of code, which will be covered below. But before we go on to do that, lets just look at why we might need these "critical sections" of code. Consider the bit of code below using System;
using System.Threading;
namespace NoLockTest
{
/// <summary>
/// This program is not thread safe
/// as it could be accessed by 2 different
/// simultaneous threads. As one thread could
/// be set item2 to 0, just at the point that
/// another thread was doing the division,
/// leading to a DivideByZeroException
/// </summary>
class Program
{
static int item1=54, item2=21;
public static Thread T1;
public static Thread T2;
static void Main(string[] args)
{
T1 = new Thread((ThreadStart)delegate
{
DoCalc();
});
T2 = new Thread((ThreadStart)delegate
{
DoCalc();
});
T1.Name = "T1";
T2.Name = "T2";
T1.Start();
T2.Start();
Console.ReadLine();
}
private static void DoCalc()
{
item2 = 10;
if (item1 != 0)
Console.WriteLine(item1 / item2);
item2 = 0;
}
}
}
This code is not thread safe, as it could be accessed by 2 different simultaneous
threads. As one thread could be set item2 to 0, just at the point that another
thread was doing the division, leading to a Now this problem may on show itself once in every 500 times you run this code, but this is the nature of threading issues, they are sometimes only exhibted once in a while so are very hard to find. You really need to think about all the eventualities before you write the code, and ensure you have the correct safe gaurds in place. Luckily we can fix this problem using locks (AKA critical sections). Shown below is a revised version of the code above. using System;
using System.Threading;
namespace LockTest
{
/// <summary>
/// This shows how to create a critical section
/// using the lock keyword
/// </summary>
class Program
{
static object syncLock = new object();
static int item1 = 54, item2 = 21;
public static Thread T1;
public static Thread T2;
static void Main(string[] args)
{
T1 = new Thread((ThreadStart)delegate
{
DoCalc();
});
T2 = new Thread((ThreadStart)delegate
{
DoCalc();
});
T1.Name = "T1";
T2.Name = "T2";
T1.Start();
T2.Start();
Console.ReadLine();
}
private static void DoCalc()
{
lock (syncLock)
{
item2 = 10;
if (item1 != 0)
Console.WriteLine(item1 / item2);
item2 = 0;
}
}
}
}
In this example we introduce the 1st of the possible techniques to create critical
sections, by the use of the Some people use I now want to briefly talk about the different ways in which you can lock (create a critical section). Lock KeywordWe have already seen the 1st example, where we used the It's worth pointing out that the Monitor ClassThe using System;
using System.Threading;
namespace LockTest
{
/// <summary>
/// This shows how to create a critical section
/// using the Monitor class
/// </summary>
class Program
{
static object syncLock = new object();
static int item1 = 54, item2 = 21;
static void Main(string[] args)
{
Monitor.Enter(syncLock);
try
{
if (item1 != 0)
Console.WriteLine(item1 / item2);
item2 = 0;
}
finally
{
Monitor.Exit(syncLock);
}
Console.ReadLine();
}
}
}
Where the Monitor.Enter(syncLock);
try
{
}
finally
{
Monitor.Exit(syncLock);
}
The MethodImpl.Synchronized attributeThe last method, relies on the use of an attribute, which you can use to adorn a method to say that it should be treated as synchronized. Let's see this using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MethodImplSynchronizedTest
{
/// <summary>
/// This shows how to create a critical section
/// using the System.Runtime.CompilerServices.MethodImplAttribute
/// </summary>
class Program
{
static int item1=54, item2=21;
static void Main(string[] args)
{
//make a call to different method
//as cant Synchronize Main method
DoWork();
}
[System.Runtime.CompilerServices.MethodImpl
(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
private static void DoWork()
{
if (item1 != 0)
Console.WriteLine(item1 / item2);
item2 = 0;
Console.ReadLine();
}
}
}
This simple example shows that you can use the One thing to note is that if you lock a whole method you are kind of missing the chance for better concurrent programming, as there will not be that much better performance, than that of a single threaded model running the method. For this reason you should try to keep the critical sections to be only around fields that need to be safe across multiple threads. So try and lock when you read/write to common fields. Of course there are some occassions where you may need to mark the entire method as a critical section, but this is your call. Just be aware that locks should generally be as small (granularity) as possible. Miscellaneous ObjectsThere are a couple of extra classes that I feel are worth a mention, these are discussed further below Interlocked"A statement is Atomic if it executes as a single indivisible instruction. Strict atomicity precludes any possible preemption. In C#, a simple read or assignment on a field of 32 bits or less is atomic (assuming a 32-bit CPU). Operations on larger fields are non-atomic, as are statements that combine more than one read.write operation:"Threading in C#, Joseph Albahari Consider the following code using System;
using System.Threading;
namespace AtomicTest
{
class AtomicTest
{
static int x, y;
static long z;
static coid Test()
{
long myVar;
x = 3; //Atomic
z = 3; //Non atomic as Z is 64 bits
myVar = z; //Non atomic as Z is 64 bits
y += x; //Non atomic read and write
x++; //Non atomic read and write
}
}
}
One way to fix this may be to wrap the non-atomic operations within a
MSDN States
using System;
using System.Threading;
namespace InterlockedTest
{
class Program
{
static long currentValue;
static void Main(string[] args)
{
//simple increment/decrement operations
Interlocked.Increment(ref currentValue);
Console.WriteLine(String.Format(
"The value of currentValue is {0}",
Interlocked.Read(ref currentValue)));
Interlocked.Decrement(ref currentValue);
Console.WriteLine(String.Format(
"The value of currentValue is {0}",
Interlocked.Read(ref currentValue)));
Interlocked.Add(ref currentValue, 5);
Console.WriteLine(String.Format(
"The value of currentValue is {0}",
Interlocked.Read(ref currentValue)));
//read a 64 bit value
Console.WriteLine(String.Format(
"The value of currentValue is {0}",
Interlocked.Read(ref currentValue)));
Console.ReadLine();
}
}
}
Which gives us the following results
The
These 2 Interlocked methods can be used for implementing lock-free (wait-free)
algorithms and data structures that would otherwise have to implemented using
full kernel locks (such as VolatileThe volatile keyword can be used on a shared field. The volatile keyword indicates that a field can be modified in the program by something such as the operating system, the hardware, or a concurrently executing thread. MSDN states "The system always reads the current value of a volatile object at the point it is requested, even if the previous instruction asked for a value from the same object. Also, the value of the object is written immediately on assignment. The volatile modifier is usually used for a field that is accessed by multiple threads without using the lock statement to serialize access. Using the volatile modifier ensures that one thread retrieves the most up-to-date value written by another thread." ReaderWriterLockSlim It is often the case that instances of a Type are thread safe in terms of
read operations, but not for updates. Although this problem could be remedied
with the use a The Put plainly, a thread holding a write lock blocks all other threads trying to obtain a read OR write lock. If no thread currently holds a write lock, any numbers of threads may obtain a read lock The main methods that the
Where * indicates that there is also a safer "try" version of the method available, which supports a timeout Let's see an slightly altered example of this (original example curtosy of Threading in C#, Joseph Albahari) using System;
using System;
using System.Threading;
using System.Collections.Generic;
namespace ReaderWriterLockSlimTest
{
/// <summary>
/// This simple class demonstrates the usage a Reader/Writer
/// situation, using the ReaderWriterLockSlim class
/// </summary>
class Program
{
static ReaderWriterLockSlim rw = new ReaderWriterLockSlim();
static List<int> items = new List<int>();
static Random rand = new Random();
static void Main(string[] args)
{
//start some readers
new Thread(Read).Start("R1");
new Thread(Read).Start("R2");
new Thread(Read).Start("R3");
//start some writers
new Thread(Write).Start("W1");
new Thread(Write).Start("W2");
}
static void Read(object threadID)
{
//do read
while (true)
{
try
{
rw.EnterReadLock();
Console.WriteLine("Thread " + threadID +
" reading common source");
foreach (int i in items)
Thread.Sleep(10);
}
finally
{
rw.ExitReadLock();
}
}
}
static void Write(object threadID)
{
//do write
while (true)
{
int newNumber = GetRandom(100);
try
{
rw.EnterWriteLock();
items.Add(newNumber);
}
finally
{
rw.ExitWriteLock();
Console.WriteLine("Thread " + threadID +
" added " + newNumber);
Thread.Sleep(100);
}
}
}
static int GetRandom(int max)
{
//lock on the Random object
lock (rand)
return rand.Next(max);
}
}
}
Which shows something simliar to the following
It should be noted that there is also a ReaderWriterLockSlim is similar to ReaderWriterLock, but it has simplified rules for recursion and for upgrading and downgrading lock state. ReaderWriterLockSlim avoids many cases of potential deadlock. In addition, the performance of ReaderWriterLockSlim is significantly better than ReaderWriterLock. ReaderWriterLockSlim is recommended for all new development. According to Sasha Goldshtein (Senior Consultant and Instructor, Sela Group),
Jeffrey Richter (famouns author of CLR Via C# book), has revised versions of
the I certainly picked the right technical reviewer, thanks Sasha.....which leads
me nicely on to the real thanks below. Special ThanksI would like to personally thank Sasha Goldshtein (Senior Consultant and Instructor, Sela Group) for his help with technically reviewing this article. Sasha Goldshtein, is someone that I chatted with as a results of the last
2 articles in this series. He always seemed to be leaving comments about things
I had wrong, or posting questions that I didn't know. So I approached Sasha,
and asked him if he would mind being the technical reviewer for the rest of
this series, which he kindly agreed to. So thanks Sasha from Sacha (me). We're DoneWell that's all I wanted to say this time. I just wanted to say, I am FULLY aware that this article borrows a lot of material from various sources, I do however feel that it could alert a potential threading newbie, to classes/objects that they simply did not know to look up. For that reason I still maintain there should be something useful in this article. Well that was the idea anyway. I just hope you agree, if so tell me, and leave a vote. Next TimeNext time we will be looking at Thread Pools. Could I just ask, if you liked this article could you please vote for it. I thank you very much Bibilography
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||