Everything about Web and Network Monitoring

Home > Industry Info > Improving .NET Application Performance Part 8: Locking and Synchronization

Improving .NET Application Performance Part 8: Locking and Synchronization

In our last article we discussed best practices when using asynchronous calls. In this article we’ll look at guidelines for Locking and Synchronization. Locking and synchronization provide a mechanism to grant exclusive access to data or code in order to avoid concurrent execution.

Determine That You Need Synchronization

Before considering synchronization options, you should think about other approaches that avoid the need of synchronization, such as loose coupling. Particularly, you do need to synchronize when multiple users concurrently need to access or update a shared resource, such as static data.

Determine the Approach

The CLR provides multiple mechanisms for locking and synchronization. You should consider the one that is right for your particular scenario:

  • Lock (C#) – The C# compiler converts the Lock statement into Monitor.Enter and Monitor.Exit calls around a try/finally block. (Use SyncLock in Visual Basic .NET.)
  • WaitHandle class. This class provides functionality to wait for exclusive access to multiple objects at the same time. There are three derivatives of WaitHandle:
    • ManualResetEvent. This allows code to wait for a signal that is manually reset.
    • AutoResetEvent. This allows code to wait for a signal that is automatically reset.
    • Mutex. This is a specialized version of WaitHandle that supports cross-process use. The Mutex object can be provided a unique name so that a reference to the Mutex object is not required. Code in different processes can access the same Mutex by name.
  • MethodImplOptions.Synchronized enumeration option – This provides the ability to grant exclusive access to an entire method, which is rarely a good idea.
  • Interlocked class -This provides atomic increment and decrement methods for types. Interlocked can be used with value types. It also supports the ability to replace a value based on a comparison.
  • Monitor object – This provides static methods for synchronizing access to reference types. It also provides overridden methods to allow the code to attempt to lock for a specified period. The Monitor class cannot be used with value types. Value types are boxed when used with the Monitor and each attempt to lock generates a new boxed object that is different from the rest; this negates any exclusive access. C# provides an error message if you use a Monitor on a value type.

Determine the Scope of Your Approach

You can lock on different objects and at different levels of granularity, ranging from the type to specific lines of code within an individual method. Identify what locks you have and where you acquire and release them. You can implement a policy where you consistently lock on the following to provide a synchronization mechanism:

  • Type. Try to avoid locking a type (for example. lock(typeof(type)). Type objects can be shared across application domains. Locking the type locks all the instances of that type across the application domains in a process. Doing so can lead to poor performance.
  • “this”. Avoid locking externally visible objects (for example. lock(this)) because you cannot be sure what other code might be acquiring the same lock, and for what purpose or policy.
  • Specific object that is a member of a class. This option the other two choices. Lock on a private static object if you need synchronization at class level. Lock on a private object (that is not static) if you need to synchronize only at the instance level for a type. Implement your locking policy consistently and clearly in each relevant method.

While locking, you should also consider the granularity of your locks. The options are as follows:

  • Method. You should consider locking at method level only when all the lines of code in the method need synchronized access; otherwise this might result in increased contention.
  • Code block in a method. Most of your requirements can be fulfilled choosing an appropriately scoped object as the lock and by having a policy where you acquire that lock just before entering the code that alters the shared state that the lock protects. By locking objects, you guarantee that only one of the pieces of code that locks the object will run at a time.

Locking and Synchronization Guidelines

This section describes the best practices when developing multithreaded code that requires locks and synchronization.

Acquire Locks Late and Release Them Early

Minimize the duration that you hold and lock resources, because most resources tend to be shared and limited. The faster you release a resource, the earlier it becomes available to other threads. Acquire a lock on the resource just before you need to access it and release the lock immediately after you are finished with it.

Avoid Locking and Synchronization Unless Required

Synchronization requires extra processing by the CLR to grant exclusive access to resources. If you do not have multithreaded access to data or require thread synchronization, do not implement it. Consider the following options before opting for a design or implementation that requires synchronization:

  • Design code that uses existing synchronization mechanisms; for example, the Cache object used by ASP.NET applications.
  • Design code that avoids concurrent modifications to data. Poor synchronization implementation can negate the effects of concurrency in your application. Identify areas of code in your application that can be rewritten to eliminate the potential for concurrent modifications to data.
  • Consider loose coupling to reduce concurrency issues. For example, consider using the event-delegation model to minimize lock contention.

Use Granular Locks to Reduce Contention

When used properly and at the appropriate level of granularity, locks provide greater concurrency by reducing contention. Consider the various options described earlier before deciding on the scope of locking. The most efficient approach is to lock on an object and scope the duration of the lock to the appropriate lines of code that access a shared resource.

Avoid Excessive Fine-Grained Locks

Fine-grained locks protect either a small amount of data or a small amount of code. When used properly, they provide greater concurrency by reducing lock contention. Used improperly, they can add complexity and decrease performance and concurrency. Avoid using multiple fine-grained locks within your code. The following code shows an example of multiple lock statements used to control three resources.

s = new Singleton();

sb1 = new StringBuilder();

sb2 = new StringBuilder();

 

s.IncDoubleWrite(sb1, sb2)

 

class Singleton

{

   private static Object myLock = new Object();

   private int count;

   Singleton()

   {

      count = 0;

   }

 

    public void IncDoubleWrite(StringBuilder sb1, StringBuilder sb2)

    {

       lock (myLock)

       {

          count++;

          sb1.AppendFormat(“Foo {0}”, count);

          sb2.AppendFormat(“Bar {0}”, count);

        }

    }

    public void DecDoubleWrite(StringBuilder sb1, StringBuilder sb2)

    {

       lock (myLock)

       {

          count–;

          sb1.AppendFormat(“Foo {0}”, count);

          sb2.AppendFormat(“Bar {0}”, count);

       }

     }

}

 

Avoid Making Thread Safety the Default for Your Type

Consider the following guidelines when deciding thread safety as an option for your types:

  • Instance state may or may not need to be thread safe. By default, classes should not be thread safe because if they are used in a single threaded or synchronized environment, making them thread safe adds additional overhead.

Adding locks to create thread safe code decreases performance and increases lock contention. In common application models, only one thread at a time executes user code, which minimizes the need for thread safety.

  • Consider thread safety for static data. If you must use static state, consider how to protect it from concurrent access by multiple threads or multiple requests. In common server scenarios, static data is shared across requests, which means multiple threads can execute that code at the same time.

Use the Fine-Grained lock (C#) Statement Instead of Synchronized

The MethodImplOptions.Synchronized attribute will ensure that only one thread is running anywhere in the attributed method at any time. However, if you have long methods that lock few resources, consider using the lock statement instead of using the Synchronized option, to shorten the duration of your lock and improve concurrency.

[MethodImplAttribute(MethodImplOptions.Synchronized)]

public void MyMethod ()

 

//use of lock

public void MyMethod()

{

    lock(mylock)

  {

   // code here may assume it is the only code that has acquired mylock

   // and use resources accordingly

     }

}

 

Avoid Locking “this”

Avoid locking “this” in your class for correctness reasons, not for any specific performance gain. To avoid this problem, consider the following workarounds:

Provide a private object to lock on.

public class A {

    lock(this) { }

  }

 

// Change to the code below:

public class A

{

  private Object thisLock = new Object();

    lock(thisLock) { }

  }

This results in all members being locked, including the ones that do not require synchronization. If you require atomic updates to a particular member variable, use the System.Threading.Interlocked class.

Coordinate Multiple Readers and Single Writers By Using ReaderWriterLock Instead of lock

A monitor or lock that is lightly contested is relatively cheap from a performance perspective, but it becomes more expensive if it is highly contested. The ReaderWriterLock provides a shared locking mechanism. It allows multiple threads to read a resource concurrently but requires a thread to wait for an exclusive lock to write the resource.

You should always try to minimize the duration of reads and writes. Long writes can hurt application throughput because the write lock is exclusive. Long reads can block the other threads waiting for read and writes.

Do Not Lock the Type of the Objects to Provide Synchronized Access

The same instance of a Type object can be used in multiple application domains without any marshaling or cloning. If you implement a policy of locking on the type of an object using lock(typeof(type)), you lock all the instances of the objects across application domains within the process.

An example of locking the whole type is as follows.

lock(typeof(MyClass))

{

  //custom code

}

Instead, you can povide a static object in your type that can be locked to provide synchronized access.

class MyClass{

  private static Object obj = new Object();

  public void SomeFunc()

  {

    lock(obj)

    {

      //perform some operation

    }

  }

}

Also avoid locking other application domain-agile types such as strings, assembly instances, or byte arrays, for the same reason.

Post Tagged with , ,
Ard-Jan Barnas

About Ard-Jan Barnas

Ard-Jan is a highly technical writer with deep knowledge into the industry. He has an international background and always brings forth articles that are not just technical but with a mix of business application. This encompassing approach to technology married to business is a welcome approach to writing.

Web & Cloud
Monitoring