.NET Micro Framework Wiki :: Synchronisation

From .NET Micro Framework Wiki
Jump to: navigation, search

Contents


Introduction

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.

  1. public static Int32 Cash;
  2.  
  3. public static void Main()
  4. {
  5.   // Start with a thousand bucks
  6.   Cash = 1000;
  7.  
  8.   // This thread adds money 
  9.   Thread tAddMoney = new Thread(new ThreadStart(AddMoney));
  10.  
  11.   // This thread gets money
  12.   Thread tGetMoney = new Thread(new ThreadStart(GetMoney));
  13.  
  14.   // Now let the threads do their work
  15.   tAddMoney.Start(); tGetMoney.Start();
  16.   while (tAddMoney.IsAlive | tGetMoney.IsAlive) 
  17.   { 
  18.     // This makes an additional load
  19.     for (int y = 0; y < 10000; y++) { }
  20.   }
  21.  
  22.   // Check the money
  23.   if (Cash != 1000)
  24.   {
  25.     Debug.Print("Ow no! Money is lost!");
  26.   }
  27.  
  28.   // Infinite sleep
  29.   Debug.Print("Cash: " + Cash.ToString());
  30.   Thread.Sleep(-1);
  31. }
  32.  
  33. public static void AddMoney()
  34. {
  35.   for (int x = 0; x < 1000; x++) 
  36.   {
  37.     Cash = Cash + 1;
  38.   }
  39. }
  40.  
  41. public static void GetMoney()
  42. {
  43.   for (int x = 0; x < 1000; x++)
  44.   {
  45.     Cash = Cash - 1;
  46.   }
  47. }

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.


Using the lock keyword

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.

  1. public static Object CachLock = new Object();
  2. public static Int32 Cash;
  3.  
  4. public static void Main()
  5. {
  6.   // Start with a thousand bucks
  7.   Cash = 1000;
  8.  
  9.   // This thread adds money 
  10.   Thread tAddMoney = new Thread(new ThreadStart(AddMoney));
  11.  
  12.   // This thread gets money
  13.   Thread tGetMoney = new Thread(new ThreadStart(GetMoney));
  14.  
  15.   // Now let the threads do their work
  16.   tAddMoney.Start(); tGetMoney.Start();
  17.   while (tAddMoney.IsAlive | tGetMoney.IsAlive) 
  18.   { 
  19.     // This makes an aditional load
  20.     for (int y = 0; y < 10000; y++) { }
  21.   }
  22.  
  23.   // Check the money
  24.   if (Cash != 1000)
  25.   {
  26.     Debug.Print("Ow no! Money is lost!");
  27.   }
  28.  
  29.   // Inifite sleep
  30.   Debug.Print("Cash: " + Cash.ToString());
  31.   Thread.Sleep(-1);
  32. }
  33.  
  34. public static void AddMoney()
  35. {
  36.   for (int x = 0; x < 1000; x++) 
  37.   {
  38.     lock (CachLock)
  39.     {
  40.       Cash = Cash + 1;
  41.     }
  42.   }
  43. }
  44.  
  45. public static void GetMoney()
  46. {
  47.   for (int x = 0; x < 1000; x++)
  48.   {
  49.     lock (CachLock)
  50.     {
  51.       Cash = Cash - 1;
  52.     }
  53.   }
  54. }

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


The Monitor class

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

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.

  1. public static Int32 Cash;
  2. public static AutoResetEvent AddReady;
  3.  
  4. public static void Main()
  5. {
  6.   // Initialize AutoResetEvent
  7.   AddReady = new AutoResetEvent(false);
  8.  
  9.   // Start with a thousand bucks
  10.   Cash = 1000;
  11.  
  12.   // This thread adds money 
  13.   Thread tAddMoney = new Thread(new ThreadStart(AddMoney));
  14.  
  15.   // This thread gets money
  16.   Thread tGetMoney = new Thread(new ThreadStart(GetMoney));
  17.  
  18.   // Now let the threads do their work
  19.   tAddMoney.Start(); tGetMoney.Start();
  20.   while (tAddMoney.IsAlive | tGetMoney.IsAlive) 
  21.   { 
  22.     // This makes an aditional load
  23.         for (int y = 0; y < 10000; y++) { }
  24.   }
  25.  
  26.   // Check the money
  27.   if (Cash != 1000)
  28.   {
  29.     Debug.Print("Ow no! Money is lost!");
  30.   }
  31.  
  32.   // Infinite sleep
  33.   Debug.Print("Cash: " + Cash.ToString());
  34.   Thread.Sleep(-1);
  35. }
  36.  
  37. public static void AddMoney()
  38. {
  39.   for (int x = 0; x < 10000; x++) 
  40.   {
  41.     Cash = Cash + 1;
  42.   }
  43.  
  44.   // All money is added
  45.   AddReady.Set();
  46. }
  47.  
  48. public static void GetMoney()
  49. {
  50.   // Wait until all money has added
  51.   AddReady.WaitOne();
  52.  
  53.   // Now get money
  54.   for (int x = 0; x < 10000; x++)
  55.   {
  56.     Cash = Cash - 1;
  57.   }
  58. }

Common Pitfalls

Personal tools