Contents |
When running multiple Threads at the same time, they all have access to exactly the same memory addresses. This means they are able to write to the same memory location at exactly the same time. Thus, when two or more threads work on the same location, you have to do some synchronisation. If you don't, your program can behave very unpredictably. Here is a program that showcases this, it is a program that simulates a bank account. At the start, it has a thousand bucks in, then two threads are started. The first thread adds 1 buck a thousand times, and the other thread removes 1 buck a thousand times. When both threads are finished, the cash is counted.
public static Int32 Cash;
public static void Main()
{ // Start with a thousand bucksCash = 1000;
// This thread adds money Thread tAddMoney = new Thread(new ThreadStart(AddMoney));
// This thread gets moneyThread tGetMoney = new Thread(new ThreadStart(GetMoney));
// Now let the threads do their worktAddMoney.Start(); tGetMoney.Start();
while (tAddMoney.IsAlive | tGetMoney.IsAlive)
{ // This makes an additional loadfor (int y = 0; y < 10000; y++) { }
} // Check the moneyif (Cash != 1000)
{Debug.Print("Ow no! Money is lost!");
} // Infinite sleepDebug.Print("Cash: " + Cash.ToString());
Thread.Sleep(-1);
}public static void AddMoney()
{for (int x = 0; x < 1000; x++)
{Cash = Cash + 1;
}}public static void GetMoney()
{for (int x = 0; x < 1000; x++)
{Cash = Cash - 1;
}}You would expect the program to end with a message that we still have a thousand bucks, but when you run the program, there is a big chance that the debug output will look like this:
The thread 0x3 has exited with code 0 (0x0). The thread 0x4 has exited with code 0 (0x0). Ow no! Money is lost! Cash: -505
This is what we expect the program to do:
Cash = 1000 | AddMoney loads the Cash Value Cash = 1000 | AddMoney adds 1 to the loaded value Cash = 1001 | AddMoney Stores the loaded value to Cash Cash = 1001 | GetMoney loads the Cash Value Cash = 1001 | GetMoney substracts 1 from the loaded value Cash = 1000 | GetMoney Stores the loaded value to Cash
But what if the thread manager decides to switch threads after the AddMoney thread loaded the Cash value? This is what happens:
Cash = 1000 | AddMoney loads the Cash Value Cash = 1000 | GetMoney loads the Cash Value Cash = 1000 | GetMoney substracts 1 from the loaded value Cash = 999 | GetMoney Stores the loaded value to Cash Cash = 1000 | AddMoney adds 1 to the loaded value Cash = 1001 | AddMoney Stores the loaded value to Cash
The problem is very clear now, the GetMoney thread successfully stored its new value but the AddMoney thread still has the initial cash value causing a wrong result.
To prevent multiple threads interfere with each other you can use the lock keyword. This is the syntax:
lock(expression) { /* Your code */ }
Where expression is a reference-type variable (like Object, String, this, typeOf(type) not: Int, Byte, Bool). What it does: Before running the code inside of the code block it checks if there is a lock present with the same expression. If not, it starts executing the code. If there is, the current thread is blocked until the other block with the same expression is finished.
Here is the same program with the lock blocks implemented. Because Cash is an Int, a separate Object is used for locking.
public static Object CachLock = new Object();
public static Int32 Cash;
public static void Main()
{ // Start with a thousand bucksCash = 1000;
// This thread adds money Thread tAddMoney = new Thread(new ThreadStart(AddMoney));
// This thread gets moneyThread tGetMoney = new Thread(new ThreadStart(GetMoney));
// Now let the threads do their worktAddMoney.Start(); tGetMoney.Start();
while (tAddMoney.IsAlive | tGetMoney.IsAlive)
{ // This makes an aditional loadfor (int y = 0; y < 10000; y++) { }
} // Check the moneyif (Cash != 1000)
{Debug.Print("Ow no! Money is lost!");
} // Inifite sleepDebug.Print("Cash: " + Cash.ToString());
Thread.Sleep(-1);
}public static void AddMoney()
{for (int x = 0; x < 1000; x++)
{lock (CachLock)
{Cash = Cash + 1;
} }}public static void GetMoney()
{for (int x = 0; x < 1000; x++)
{lock (CachLock)
{Cash = Cash - 1;
} }}When the program is run we get the expected output:
The thread 0x3 has exited with code 0 (0x0). The thread 0x4 has exited with code 0 (0x0). Cash: 1000
For synchronisation, C# has the Monitor class. The lock keyword also uses the Monitor class internally. When you use lock, C# internally transforms the code into this:
Monitor.Enter(expression); try { // Code inside lock block } finally { Monitor.Exit(myLock) }
First, the critical section is started by Monitor.Enter. Next, your code is executed. The Monitor.Exit is placed in a finally block, so that it is executed even when an exception is thrown.
ManualResetEvent and AutoResetEvent allow threads to signal each other. It's typically used when a task wants to signal to another task that it's finished. Both types store a boolean state that can be altered by the Set and Reset commands. Threads can wait for a Set state by calling WaitOne. This command blocks the current thread until the state changes to true. The difference between the two types is that AutoResetEvent resets the state to false when it has released a waiting thread.
Below is the bank example from the beginning of this article. An AutoResetEvent is added. It is initialized with a state of false in the main program block. The GetMoney thread monitors the state with the WaitOne command. This command blocks the thread until the AutoResetEvent state is true. The AddMoney thread does it's work and Set the AutoResetEvent. This sets the AutoResetEvent state to true so that the GetMoney thread can do it's work.
public static Int32 Cash;
public static AutoResetEvent AddReady;
public static void Main()
{ // Initialize AutoResetEventAddReady = new AutoResetEvent(false);
// Start with a thousand bucksCash = 1000;
// This thread adds money Thread tAddMoney = new Thread(new ThreadStart(AddMoney));
// This thread gets moneyThread tGetMoney = new Thread(new ThreadStart(GetMoney));
// Now let the threads do their worktAddMoney.Start(); tGetMoney.Start();
while (tAddMoney.IsAlive | tGetMoney.IsAlive)
{ // This makes an aditional loadfor (int y = 0; y < 10000; y++) { }
} // Check the moneyif (Cash != 1000)
{Debug.Print("Ow no! Money is lost!");
} // Infinite sleepDebug.Print("Cash: " + Cash.ToString());
Thread.Sleep(-1);
}public static void AddMoney()
{for (int x = 0; x < 10000; x++)
{Cash = Cash + 1;
} // All money is addedAddReady.Set();
}public static void GetMoney()
{ // Wait until all money has addedAddReady.WaitOne();
// Now get moneyfor (int x = 0; x < 10000; x++)
{Cash = Cash - 1;
}}