Knowledge is power. We love to share it.

News related to Mono products, services and latest developments in our community.

pajo

Thread synchronization in .NET

05/10/2012

If you have ever built a web application you were working in a multi-threaded environment. Sooner or later you’re going to run into situations where you need to synchronize threads. This is done by locking sections of your code, first thread to arrive will acquire a lock, while other threads will wait for the first thread to release the lock.  So lets see how we can do it in .NET.

Lock statement and Monitor

The most common way to lock a critical section uses a lock statement in c# (SyncLock in VB). You need to define an object that will be acquiring the lock and inside the lock block you will define your critical section. Lock is acquired on an object instance so you should read guidelines on how to use lock statements to avoid some possible deadlock issues. Locks are very simple to implement and critical code is closed inside lock block, isolating critical sections. You don’t need to worry about acquiring and releasing a lock - it is done automatically. For most common tasks, this technique will be sufficient and it’s preferred way for synchronizing threads.

Monitor is very similar to the lock statement, in fact the standard lock statement is wrapping Monitor Enter and Exit methods. The only real advantage over the lock statement is that you can create locks on demand. You need to explicitly acquire lock using Enter method and release it using Exit method. Failing to call Exit can leave lock hanging and blocking all other threads. Both of them can be used only for synchronizing threads inside one process.

Mutex and Semaphores

Mutex and Semaphores can be used to protect access to the shared resources. Note that Mutex will grant exclusive rights to the resource, while Semaphore can limit the number of threads that can access resource and are usually used to protect resource pools. Both of them can be unnamed or named, unnamed being visible only to the process that created them(local), while named instances are visible throughout the operating system(global). Mutexes are of very limited use in web applications as there is usually no need for process synchronization - for local synchronization you will almost always use locks as a better alternative. On the other hand, Semaphores can be handy in situations where you want to protect some external resource like a service or a specific device from overload.

Database lock

Recently I had to add a discount coupon system to the application. Coupons can only be used with payment transactions and I had to ensure that payment is complete before marking coupon as used. Completing payment process can be slow and it’s possible to end up processing different payments with the same coupon. If a coupon is already used I had to abort the transaction and on the other hand marking coupon as used before transaction is complete can be dangerous (if for any reason you fail to recover from a failure). The easist way to implement this properly was to lock the payment processing process if the same coupon is used. Since this application was designed to run on a web farm, it was no easy task to do this. Fortunately for me, if you’re running your application on SQL Server (like in this case) you can create application lock on a database. There are two stored procedures, sp_getapplock and sp_releaseapplock, that are used to get and release a lock. To make it more easier to use I have created a class similar to Mutex.

public class DBMutex : System.Threading.WaitHandle
{
    private const int defaultTimeout = 40000;
    public string Name { get; private set; }
    private System.Data.SqlClient.SqlConnection Connection { get; set; }
    private System.Data.SqlClient.SqlTransaction Transaction { get; set; }
 
    public DBMutex(System.Data.SqlClient.SqlConnection connection, string name)
    {
        this.Connection = connection;
        this.Name = name;
    }
 
    public override bool WaitOne()
    {
        return this.WaitOne(defaultTimeout);
    }
 
    public override bool WaitOne(int millisecondsTimeout)
    {
        return this.WaitOne(millisecondsTimeout, false);
    }
    public override bool WaitOne(int millisecondsTimeout, bool exitContext)
    {
        return this.WaitOne(TimeSpan.FromMilliseconds(millisecondsTimeout), exitContext);
    }
 
    public override bool WaitOne(TimeSpan timeout)
    {
        return this.WaitOne(timeout, false);
    }
 
    public override bool WaitOne(TimeSpan timeout, bool exitContext)
    {
        Connection.Open();
        var command = Connection.CreateCommand();
        this.Transaction = Connection.BeginTransaction("applock");
        command.Connection = this.Connection;
        command.Transaction = this.Transaction;
        command.CommandText = String.Format(@"EXEC sp_getapplock                
            @Resource = '{0}',
            @LockMode = 'Exclusive',
            @LockOwner = 'Transaction',
            @LockTimeout = {1},
            @DbPrincipal = 'public'", this.Name, (long)timeout.TotalMilliseconds);
        command.ExecuteNonQuery();
 
        return true;
    }
 
    public void ReleaseDBMutex()
    {
        var command = Connection.CreateCommand();
        command.Connection = this.Connection;
        command.Transaction = this.Transaction;
        command.CommandText = String.Format(@"EXEC sp_releaseapplock
                        @Resource = '{0}',
                        @DbPrincipal = 'public',
                        @LockOwner = 'Transaction'", this.Name);
        command.ExecuteNonQuery();
        this.Transaction.Commit();
        Connection.Close();
    }
 
    protected override void Dispose(bool explicitDisposing)
    {
       if (Transaction != null)
            ReleaseDBMutex();
        Connection.Dispose();
 
        base.Dispose(explicitDisposing);
    }
     
}

This way, I was able to isolate the code so that only one transaction with the same coupon is processed, while all other transactions can be executed in parallel. I was also able to mark a coupon as used without fear that another thread is using  it while waiting for transaction to complete.

Locking problems

The most important thing you have to remember when working with locks is to release alock at some point. Failing to do so can block your application. It’s best to keep critical code inside the try catch block and release it in a finally block. Also, keep in mind that locking will add some overhead to your code execution, but it is usually not an issue. You should always take care to lock only critical parts of your code and release locks as soon as possible, or you can end up with threads waiting for their turn to execute and creating large waiting queues which in turn can seriously affect performance. Of course, there is also a problem with deadlocks, where two or more threads are waiting for each other. All of these issues are hard to find and debug, but deadlocks can be a real nightmare to recognize and resolve. So, make sure you always follow locking guidelines and place your locks carefully, to minimize chances for runtime problems.

Rated 1.53, 19 vote(s).