Programming with multiple threads is not easy. When starting multiple threads that access the same data, you can get intermittent 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. Wediscuss two in parti~ next race conditions and deadlocks ..
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 does, 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 cinethread has just completed the if (state == 5) statement, it might be preempted, and the scheduler will 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.
Let’s verify this by defining a thread method. The method RaceCondi tion () of the class SampleThread gets a StateObject as a parameter. Inside an endless while loop, the ChangeState () method is invoked. The variable i is used just to show the loop number in the assert message:
In the Main () method of the program, a new Sta teObj ect is created that is shared between all the threads. Thread objects are created by passing the address of RaceCondi tion with an object of type SampleThread in the constructor of the Thread class. The thread is then started with the Start ( ) method, passing ~e state object.
When you start the program, you will get race conditions. How long it takes until the first race condition happens depends on your system and whether 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 occur with a single-core CPU because thread scheduling is preemptive, but not that often.
Figure 19-2shows 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.
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. Because this object is-shared between all threads, a thread must wait at the lock if anotherthread 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 ~th the state variable is using a lock, the race condition no longer occurs.
Instead of doing the lock when using the shared object, yo~ can make the shared 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 lynC of type object is defined. and used with the lock statement. If a lock is done using the same synchronization object every time the value state is changed, race conditions no longer happen
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
To demonstrate deadlocks, two objects of type StateObj ect are instantiated and passed with the conseructor of the SampleThread class. Two threads are created: one thread running the method
Deadlockl () and the other thread running the method Deadlock2 ():
StateObject state1 = new StateObject();
StateObject state2 = new StateObject();
new Thread(new SampleThread(state1, state2) .Deadlock1) .Start();
new Threa~(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 sl and next for 82. The method DeadlO’Ck2() first does a lock for 52 and then for 51. Now it may happen from time to time that the lock for.ai in Dead1ock1 () is resolved. Next, a thread switch occurs, and Deadlock2 () starts to run
and gets the lock for sz. 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.
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. Again, how soon the problem happens depends on your system configuration. And the result will diHer from time to time. The problem of deadlocks is not always as obvious as it is here. One thread locks sl and then s2; the other thread locks 82 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 prevent 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, which we show in the next section.