Threading Issues


Programming with multiple threads is not easy. When starting multiple threads that access the same data, you can get intermediate problems that are hard to find. To avoid getting into trouble, you must pay attention to synchronization issues and the problems that can happen with multiple threads. Issues with threads such as race conditions and deadlocks are discussed next.

Race Condition

A race condition can occur if two or more threads access the same objects and access to the shared state is not synchronized.

To demonstrate a race condition, the class StateObject with an int field and the method ChangeState are defined. In the implementation of ChangeState, the state variable is verified if it contains 5. If it contains 5, the value is incremented. Trace.Assert is the next statement that immediately verifies that state now contains the value 6. After incrementing a variable by 1 that contains the value 5, you might expect that the variable now has the value 6. But this is not necessarily the case. For example, if one thread has just completed the if (state == 5) statement, it might be preempted and the scheduler run another thread. The second thread now goes into the if body and because the state still has the value 5, the state is incremented by 1 to 6. The first thread is now scheduled again, and in the next statement the state is incremented to 7. This is when the race condition occurs and the assert message is shown.

  public class StateObject {    private int state = 5;    public void ChangeState(int loop)    {       if (state == 5)       {          state++;          Trace.Assert(state == 6, "Race condition occurred after " +                loop + " loops");       }       state = 5;    } } 

Let’s verify this by defining a thread method. The method RaceCondition() of the class SampleThread gets a StateObject as parameter. Inside an endless while loop, the ChangeState() method is invoked. The variable i is just used to show the loop number in the assert message.

  public class SampleThread {    public void RaceCondition(object o)    {       Trace.Assert(o is StateObject, "o must be of type StateObject");       StateObject state = o as StateObject;              int i = 0;       while (true)       {          state.ChangeState(i++);       }    } } 

In the Main method of the program, a new StateObject is created that is shared between all the threads. Thread objects are created by passing the address of RaceCondition with an object of type SampleThread in the constructor of the Thread class. The thread is then started with the Start() method, passing the state object.

  static void Main() {    StateObject state = new StateObject();    for (int i = 0; i < 20; i++)    {       new Thread(new SampleThread().RaceCondition).Start(state);    } } 

Starting the program you will get race conditions. How long it takes after the first race condition happens depends on your system and if you build the program as a release or debug build. With a release build, the problem will happen more often because the code is optimized. If you have multiple CPUs in your system or dual-core CPUs where multiple threads can run concurrently, the problem will also occur more often than with a single-core CPU. The problem will also occur with a single-core CPU as thread scheduling is preemptive, just not that often.

Figure 18-2 shows an assert of the program where the race condition occurred after 3,816 loops. You can start the application multiple times, and you will always get different results.

image from book
Figure 18-2

You can avoid the problem by locking the shared object. You can do this inside the thread by locking variable state that is shared between the threads with the lock statement as shown. Only one thread can be inside the lock block for the state object. As this object is shared between all threads, a thread must wait at the lock if another thread has the lock for state. As soon as the lock is accepted, the thread owns the lock and gives it up with the end of the lock block. If every thread changing the object referenced with the state variable is using a lock, the race condition does not occur anymore.

 public class SampleThread {    public void RaceCondition(object o)    {       Trace.Assert(o is StateObject, "o must be of type StateObject");       StateObject state = o as StateObject;       int i = 0;       while (true)       {          lock (state)  // no race condition with this lock          {             state.ChangeState(i++);          }       }    } }

Instead of doing the lock when using the shared object, you can also make the share object thread-safe. Here, the ChangeState() method contains a lock statement. Because you cannot lock the state variable itself (only reference types can be used for a lock), the variable sync of type object is defined and used with the lock statement. If every time the value state is changed a lock using the same synchronization object is done, race conditions no longer happen.

 public class StateObject {    private int state = 5;    private object sync = new object();    public void ChangeState(int loop)    {       lock (sync)       {          if (state == 5)          {             state++;             Trace.Assert(state == 6, "Race condition occurred after " +                   loop + " loops");          }          state = 5;       }    } }

Deadlock

Too much locking can get you in trouble as well. In a deadlock, at least two threads halt and wait for each other to release a lock. As both threads wait for each other, a deadlock occurs and the threads wait endlessly.

To demonstrate deadlocks, two objects of type StateObject are instantiated and passed with the constructor of the SampleThread class. Two threads are created, one thread running the method Deadlock1(), the other thread running the method Deadlock2().

 StateObject state1 = new StateObject(); StateObject state2 = new StateObject(); new Thread(new SampleThread(state1, state2).Deadlock1).Start(); new Thread(new SampleThread(state1, state2).Deadlock2).Start();

The methods Deadlock1() and Deadlock2() now change the state of two objects s1 and s2. That’s why two locks are done. The method Deadlock1() first does a lock for s1 and next for s2. The method Deadlock2() first does a lock for s2 and then for s1. Now it may happen from time to time that the lock for s1 in Deadlock1() is resolved. Next a thread switch occurs, and Deadlock2() starts to run and gets the lock for s2. The second thread now waits for the lock of s1. Because it needs to wait, the thread scheduler schedules the first thread again, which now waits for s2. Both threads now wait and don’t release the lock as long as the lock block is not ended. This is a typical deadlock.

  public class SampleThread {    public SampleThread(StateObject s1, StateObject s2)    {       this.s1 = s1;       this.s2 = s2;    }    private StateObject s1;    private StateObject s2;    public void Deadlock1()    {       int i = 0;       while (true)       {          lock (s1)          {             lock (s2)             {                s1.ChangeState(i);                s2.ChangeState(i++);                Console.WriteLine("still running, {0}", i);             }          }       }    }    public void Deadlock2()    {       int i = 0;       while (true)       {          lock (s2)          {             lock (s1)             {                s1.ChangeState(i);                s2.ChangeState(i++);                Console.WriteLine("still running, {0}", i);             }          }       }    } } 

As a result, the program will run a number of loops and will soon be unresponsive. The message still running is just written a few times to the console. How soon the problem happens depends on your system configuration again. And the result will differ from time to time.

The problem of deadlocks is not always as obvious as it is here. One thread locks s1 and then s2; the other thread locks s2 and then s1. You just need to change the order so that both threads do the lock in the same order. However, the locks might be hidden deeply inside a method. You can avoid this problem by designing a good lock order from the beginning in the architecture of the application, and also by defining timeouts for the locks. How you can define timeouts is shown in the next section.




Professional C# 2005 with .NET 3.0
Professional C# 2005 with .NET 3.0
ISBN: 470124725
EAN: N/A
Year: 2007
Pages: 427

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net