Synchronization
We need some sort of synchronization mechanism, whereby, when we have any variable (or any resource for that matter – a file on the hard disk for example) which is simultaneously being read from and written to by multiple threads, we can be sure that the data in that variable or resource will remain consistent.
Have a look at the following code (better still, create a new console application project, and use the following code):
class BankAccount
{
public int Money;
}
class Program
{
static Mutex myMutex;
static void
Main (string[] args)
{
BankAccount account = new BankAccount();
account.Money = 100;
myMutex = new Mutex();
Thread oneDollarThread = new Thread(new ParameterizedThreadStart(AddOneDollar));
Thread twoDollarThread = new Thread(new ParameterizedThreadStart(AddTwoDollars));
oneDollarThread.Start(account);
twoDollarThread.Start(account);
Thread.Sleep(4000);
Console.WriteLine("Amount of money in account = " + account.Money);
}
static void AddOneDollar(object param)
{
BankAccount account = (BankAccount)param;
for (int i = 1; i <= 100; i++)
{
myMutex.WaitOne();
int oldamount = account.Money;
oldamount = oldamount + 1;
Thread.Sleep(1);
account.Money = oldamount;
myMutex.ReleaseMutex();
}
}
static void AddTwoDollars(object param)
{
BankAccount account = (BankAccount)param;
for (int i = 1; i <= 100; i++)
{
myMutex.WaitOne();
int oldamount = account.Money;
oldamount = oldamount + 2;
Thread.Sleep(1);
account.Money = oldamount;
myMutex.ReleaseMutex();
}
}
}
Run it. Let’s look at the output I got:
Ok, things seem back to normal. Let’s now look at the code.
The first thing I’ve done differently is to create an object of the class Mutex. The word mutex comes from “mutual exclusion” – it is used to ensure that a piece of code which is being executed, is being executed at any instant in time by at most one thread. So how do we use this object to ensure this? Have a look at the rest of the code.
A Mutex variable can be “owned” by at most one thread at any instant of time. When we initially created the mutex, it was not owned by anyone (rather, it was not owned by any thread). Whenever the AddOneDollar or the AddTwoDollars function want to update the Money field in the global account variable, we first execute the line
myMutex.WaitOne();
What this means is, we want the current thread to try and take ownership of the Mutex variable. If at that instant, any other thread has the ownership of the Mutex variable, then the current thread will be “blocked” (i.e., the WaitOne() function will not return) until the thread that owns the Mutex “releases” it. Once the current thread obtains the ownership of the Mutex, the WaitOne() function returns. Execution then proceeds to the next line, where we read the Money field, and increment it. Now, suppose that before the thread was able to update the Money field, the Operating System paused it (in order to give other threads a chance to execute), and gave control to our other thread. However, our other thread will reach the line
myMutex.WaitOne();
and get “blocked”, since the mutex is at present owned by our first thread, which has yet not given up ownership of it. Our first thread, when it ultimately gets back control, will then update the Money field, and then execute
myMutex.ReleaseMutex()
By now you should be able to figure out what the above line does. Yes, it releases ownership of the mutex, allowing our other thread to obtain ownership of it. In this way, a mutex can be used to synchronize access to shared resources.
The “lock” syntax
What if you forget to Release a mutex? Simple answer – your code isn’t going to run properly. You need to make sure that you always release your mutexes, even when exceptions are thrown from code that gets executed between the WaitOne() and ReleaseMutex() calls. Bottom line – you need to make sure that all paths that your code follows always release any mutexes which the code might have taken ownership of.
Since this is a slightly cumbersome problem (but a much more difficult one to debug), there is another synchronization mechanism which you can use. Have a look at the code below:
class BankAccount
{
public int Money;
}
class Program
{
static object lockObject;
static void
Main (string[] args)
{
BankAccount account = new BankAccount();
account.Money = 100;
lockObject = new object();
Thread oneDollarThread = new Thread(new ParameterizedThreadStart(AddOneDollar));
Thread twoDollarThread = new Thread(new ParameterizedThreadStart(AddTwoDollars));
oneDollarThread.Start(account);
twoDollarThread.Start(account);
Thread.Sleep(4000);
Console.WriteLine("Amount of money in account = " + account.Money);
}
static void AddOneDollar(object param)
{
BankAccount account = (BankAccount)param;
for (int i = 1; i <= 100; i++)
{
lock (lockObject)
{
int oldamount = account.Money;
oldamount = oldamount + 1;
Thread.Sleep(1);
account.Money = oldamount;
}
}
}
static void AddTwoDollars(object param)
{
BankAccount account = (BankAccount)param;
for (int i = 1; i <= 100; i++)
{
lock (lockObject)
{
int oldamount = account.Money;
oldamount = oldamount + 2;
Thread.Sleep(1);
account.Money = oldamount;
}
}
}
}
I’ve created an object called lockObject, which I then use from the AddOneDollar and AddTwoDollar functions. Within these functions, before accessing the global account variable, I execute the line
lock (lockObject)
and then execute the rest of the code within the scope. You can think of this lock statement as something similar to Mutexes – only one thread can lock an object at one time. If a second thread tries to lock an object which has been locked by another thread, then the first thread will be “blocked” until the second thread releases the lock. And when exactly is the lock “released”? You’ll see in the code above that we use curly braces, as if we’re defining a scope. The object remains “locked” as long as the code within the scope is being executed. Once you leave the scope, the lock is automatically “released”. And here comes the advantage of locks over mutexes – the moment you leave the scope, the lock is automatically released – you don’t need to do anything on your part. Whether you leave the scope via a return statement, or whether an exception is thrown, you don’t need to bother - .NET takes care of the lock releasing mechanism for you.
Attachments
Project Files Thread Synchronization Sample Projects