Introduction
Multithreaded applications provide the illusion that numerous activities are happening at more or less the same time. In C#, the System.Threading
namespace provides a number of types that enable multithreaded programming.
Concurrency is one of the key concepts when more than one thread accesses a shared data. Uncontrolled concurrent access to the object may either leave it in an indeterminate state, which leads to runtime exceptions, or the object will behave unexpectedly and generate random, garbage output.
In three occasions, a shared object does not need synchronization.
- If the object is written but never read.
- If the object is read but never written.
- If only a single thread accesses the object at any given time.
An object that does not fall into one of these conditions (and most of real objects will not, for God's sake, who wants to have an object that he writes but never reads and vice versa) should to be thoroughly analyzed and properly synchronized if it is to be used, in a multithreaded environment.
Purpose of this Article
Let us assume that you have designed a thread-silly object which is supposed to run in a single-threaded environment. Then all of a sudden, your project specs have changed (remember that customers are -and your boss is- always right :)). Now your poor innocent object has found itself unprotected in a multi-threaded environment.
This article discusses what may happen to a thread-silly object in a multi-threaded environment and proposes a method (e.g. using a synchronized wrapper) for safely using it in this multi-threaded environment without changing its internal structure too much.
When to use Synchronization?
Synchronizing an object is done via Monitor
s and monitoring brings an overhead to the application. So if your object will not be executed in a multi-threaded environment, using thread-unaware objects will be more efficient than their thread-aware counterparts.
I would like to expand the concept of thread-awareness a bit:
Thread-Awareness and Thread-Safety
Most of the time people use thread-aware/thread-safe words interchangeably. However, there is a slight difference between them. Despite my thorough Googling on the topic, I was unable to find a clear written definition. People appear to have their own subjective interpretations. After reading here and there and everywhere for definitions on the subject, here follows my boiled up definition for them: (I am open to and will appreciate any contributions on the terms)
Thread aware:
At any given time, at most one thread can be active on the object. The object is aware of the threads around it and protects itself from the threads by putting all the threads in a queue. Since there can be only a single thread active on the object at any given time, the object will always preserve its state. There will not be any synchronization problems.
Thread safe:
At a given time, multiple threads can be active on the object. The object knows how to deal with them. It has properly synchronized access to its shared resources. It can preserve its state data in this multi-threaded environment (i.e. it will not fall into intermediate and/or indeterminate states). It is safe to use this object in a multi-threaded environment.
Using an object that is neither thread-aware nor thread-safe may result in getting incorrect and random data and mysterious exceptions (due to trying to access the object when it is being used by a thread and is in an unstable, in-between state at the instant of access of the second thread).
A Simple Interface
Let us begin by creating a simple interface:
namespace com.sarmal.articles.en.synchronization
{
using System;
public interface BankAccount
{
void Empty();
void Add(double money);
double Balance {get;}
bool IsSynchronized {get;}
object SyncRoot {get;}
}
}
Not a big surprise huh? Empty()
method clears the balance, Add(double)
adds money to the bank account, and Balance
is the total amount of money currently deposited in the account.
The two other elements that require more attention are IsSynchronized
and SyncRoot
. IsSynchronized
method returns whether the object is safe for multi-threaded access, and SyncRoot
is the synchronization root of the object which you can pass to lock
statement as a parameter.
lock(acc.SyncRoot) {
... critical code goes here ...
}
Implementation Class
The implementation of the interface is not a big issue. There is a private double
member variable, Add
method adds to it, Empty
sets it to zero, and Balance
returns what is currently stored in that variable.
One thing that is noteworthy is the Add
method:
public virtual void Add(double money) {
double temp = sum;
Thread.Sleep(0);
temp += money;
sum = temp;
}
Note that those four lines of code is equivalent to nothing but sum += money
. We have split the statements into lines and added a Thread
sleep in between to increase the probability of getting concurrency-related errors. Add(double money) {sum+=money;}
can also serve the purpose of this article, however the code presented above leads to a more dramatic and distinct outcome.
The Overridden Synchronized Methods
Things that take attention in the implementation class AccountImpl
are the two overridden Synchronized
methods:
public static BankAccountImpl Synchronized(BankAccountImpl impl) {
return (BankAccountImpl) Wrap(impl);
}
public static BankAccount Synchronized(BankAccount acc) {
return (BankAccount) Wrap(acc);
}
The private
method Wrap
returns the passed BankAccount
object itself if the parameter is synchronized, otherwise it returns a synchronized wrapper class SyncAccount
which extends BankAccountImpl
.
The wrapper class SyncAccount
, which is the key point of this discussion, is a private sealed
inner class. It stores the reference of the BankAccount
as a private
member and locks critical portions of the code using the SyncRoot
of the member.
private sealed class SyncAccount:BankAccountImpl {
private object syncRoot;
private BankAccount bankAccount;
public SyncAccount(BankAccount acc) {
bankAccount = acc;
syncRoot = acc.SyncRoot;
}
public override void Empty() {
lock(syncRoot) {
bankAccount.Empty();
}
}
... truncated ...
public override bool IsSynchronized {get {return true;}}
public override object SyncRoot {get {return syncRoot;}}
}
The Test Case
Test.cs includes the Test
class to test the application. If you open it, you will see a commented out code in its main
method.
acc = new BankAccountImpl();
When you build the project and run, it will generate an output similar to the following:
Total balance is expected to be: 120.
Starting Thread-0
Starting Thread-1
Starting Thread-2
Thread-0 entered add.
Thread-0: Balance before add : 0
... truncated ...
Starting Thread-4
Thread-0: Balance after add : 4
Thread-0: Balance before add : 4
Thread-1: Balance after add : 4
Thread-1: Balance before add : 4
Thread-0: Balance after add : 5
Thread-0: Balance before add : 5
Thread-1: Balance after add : 5
Thread-1: Balance before add : 5
... truncated ...
Thread-9: Balance after add : 30
Thread-9 exited add.
Thread-11: Balance after add : 30
Thread-11: Balance before add : 30
Joining Thread-10
Joining Thread-11
Thread-11: Balance after add : 31
Thread-11 exited add.
The current balance is 31.
The outcome will be different for each run but the trend will be the same. Current balance will always be less than the expected balance.
The situation may be seen somewhat analogous to the good old producer-consumer dilemma (in reverse). At a given instant of time, more than one thread time read (i.e. consumed) the shared variable. The threads increment the value they had read (i.e., not the original value but the snapshot they took) by one and store it back (i.e., produced).
Now let us uncomment the commented out parts, and rebuild the solution.
acc = new BankAccountImpl();
acc = BankAccountImpl.Synchronized(acc);
We will get a synchronized wrapper around the class, and everything will be as expected. Only one thread will be able to access each method at any given time, which will ensure data integrity.
Here is what the outcome will look like after rebuild:
Total balance is expected to be: 120.
Starting Thread-0
Starting Thread-1
Starting Thread-2
Thread-1 entered add.
Thread-1: Balance before add : 0
Thread-1: Balance after add : 1
Thread-1: Balance before add : 1
Starting Thread-3
Thread-1: Balance after add : 2
Thread-1: Balance before add : 2
Thread-1: Balance after add : 3
Thread-1: Balance before add : 3
Thread-0 entered add.
Thread-2 entered add.
Thread-0: Balance before add : 4
Thread-0: Balance after add : 5
Thread-0: Balance before add : 5
Starting Thread-4
Thread-0: Balance after add : 6
Thread-0: Balance before add : 6
Starting Thread-5
Thread-0: Balance after add : 7
Thread-0: Balance before add : 7
... truncated ...
Joining Thread-5
Joining Thread-6
Joining Thread-7
Joining Thread-8
Joining Thread-9
Joining Thread-10
Joining Thread-11
The current balance is 120.
Conclusion
Synchronization is an important factor to preserve data concurrency in a multithreaded environment. No matter in which language you code, whether it is ANSI C or C# or Java or anything else, if there are multiple processes that rush to gain control of a shared resource, careful analysis of the situation is extremely necessary. Real-world multi-threaded scenarios are not as simple as the above Account example. Yet the .NET framework makes the threading and synchronization easy to handle. To be honest, IMHO, the threading capabilities of C# beats Java.
So What's Next ?
Rather than diving threading stuff in great detail, I preferred to discuss basic issues around a sample application. I spare a detailed examination of System.Threading
namespace's methods, advanced issues such as caching, memory models, memory barriers, lazy initialization, and conceptual topics such as deadlocks, atomicity, thread-safety, race conditions, semaphores, mutexes, critical sections etc. etc... to my proceeding articles. Else I would be rather boring and would be consuming too much paper space.
Happy coding!
History
- 2005-02-21
- 2005-02-24
- Added some descriptive text, modified the code (added some extra console prints to describe what's going on.)
- Created XML documentation
- Uploaded the revised demo project.
- 2005-02-26
- Modified the article according to the revised code.