You have heard many times that managing multithreading application is difficult. Let’s check one of the most common problems and how we can handle the problem.
1. Bank Account Problem
Let’s check the classic problem of multitasking – 2 people are withdrawing money from the same account.
Suppose each “withdraw” task is performed in a new thread.
public class BankAccount { public decimal Balance { get; set; } public void Withdraw(decimal amount) { Console.WriteLine("{0} - Before: {1:c}", Thread.CurrentThread.ManagedThreadId, Balance); Thread.Sleep(2000); // takes 2 secs Balance -= amount; Console.WriteLine("{0} - After: {1:c}", Thread.CurrentThread.ManagedThreadId, Balance); } } public static class LockTest { public static void Test1() { BankAccount acc = new BankAccount { Balance = 1000M }; // $1000 // Person 1 Thread withdraw1 = new Thread(param => { BankAccount account = param as BankAccount; account.Withdraw(100M); }); // Person 2 Thread withdraw2 = new Thread(param => { BankAccount account = param as BankAccount; account.Withdraw(100M); }); Console.WriteLine("Balance = {0:c}", acc.Balance); withdraw1.Start(acc); withdraw2.Start(acc); withdraw1.Join(); withdraw2.Join(); Console.WriteLine("Balance = {0:c}", acc.Balance); } }
You can find the final balance is $900, not $800. It is good for you but the bank will not be happy about it.
2. Locks
when a single object can be modified by multiple threads, you need to take some actions to make the object status in sync.
The most common way is to lock the part of the code. The “lock” keyword requires you to specify a token (an object reference) that must be acquired by a thread to enter within the lock scope.
public class BankAccount { private object lockKey = new object(); public decimal Balance { get; set; } public void Withdraw(decimal amount) { lock (lockKey) { Console.WriteLine("{0} - Before: {1:c}", Thread.CurrentThread.ManagedThreadId, Balance); Thread.Sleep(2000); // takes 2 secs Balance -= amount; Console.WriteLine("{0} - After: {1:c}", Thread.CurrentThread.ManagedThreadId, Balance); } } }
Now the second thread should wait until the first thread exits out of the lock scope and frees the lock.
You can see the final balance is $800 now.
3. Monitor Class
The “lock” keyword is a shortcut syntax of the “System.Threading.Monitor” class.
public static class Monitor { public static void Enter(Object obj); public static void Exit(Object obj); }
You can rewrite the “BankAccount” class like this:
public void Withdraw(decimal amount) { Monitor.Enter(lockKey); try { Console.WriteLine("{0} - Before: {1:c}", Thread.CurrentThread.ManagedThreadId, Balance); Thread.Sleep(2000); // takes 2 secs Balance -= amount; Console.WriteLine("{0} - After: {1:c}", Thread.CurrentThread.ManagedThreadId, Balance); } finally { Monitor.Exit(lockKey); } }
The “Monitor” class provide some other methods such as “Wait()” and “Pulse()” to fine-tune the behavior of the lock but in most cases the “lock” keyword will suffice to meet your needs.
4. [Synchronization] Attribute
The “System.Runtime.Remoting.Contexts.SynchronizationAttribute” can be used to enforce a synchronization for all contexts that share the same object. You can specify the [Synchronization] Attribute to the class.
The important thing to remember is that the [Synchronization] Attribute works only for “System.ContextBoundObject” object. Your class should inherit from “ContextBoundObject” class.
[Synchronization()] public class BankAccount : ContextBoundObject { public decimal Balance { get; set; } public void Withdraw(decimal amount) { Console.WriteLine("{0} - Before: {1:c}", Thread.CurrentThread.ManagedThreadId, Balance); Thread.Sleep(2000); // takes 2 secs Balance -= amount; Console.WriteLine("{0} - After: {1:c}", Thread.CurrentThread.ManagedThreadId, Balance); } }