Johann Gerell
Johann Gerell

Reputation: 25591

How expensive is lock(...) when the lock isn't contended?

While looking into double-checked locking I've seen numerous recommendations to just skip the first check and immediately go for the lock and just check after taking it instead.

This lead me to wonder, is lock (this.padlock) "cheap" when uncontended?

Upvotes: 23

Views: 1736

Answers (3)

Tony Tanzillo
Tony Tanzillo

Reputation: 141

I wrote this very quickly and haven't tested it, and I'm posting it only to convey the general idea, of one approach to conditional locking, using extension methods, and delegates or lambda functions. I also can't say without testing, if this would be self-defeating, which is certainly possible.

I have code that runs in both multi-threaded processes, and processes that use the cooperative fiber mode threading model (e.g., where there are multiple threads that never execute asynchronously). In the cooperative fiber mode app, locking is pointless and wasteful, so this may be a solution to avoiding lots of messy logic at each point where locking must be done.

// Conditional Locking concept code

namespace SystemExtensions {

public static class LockMeUp
{
  private static bool isLockingEnabled = true;

  // If set to true, locking will be performed
  // by the extension methods below.

  internal static bool LockingEnabled
  {
    get
    {
      return isLockingEnabled;
    }
    set
    {
      isLockingEnbaled = value;
    }
  }

  static void CheckNull<TLock>( TLock target ) where TLock: class
  {
    if( target == null )
      throw new ArgumentNullException("target cannot be null");
  }

  // Invoke the supplied action on the supplied lock object

  public static void TryLock<TLock>( 
    this TLock target, 
    Action<TLock> action ) where TLock: class
  {
    CheckNull( target );
    if( isLockingEnabled )
    {
      lock( target )
      {
        action( target );
      }
    }
    else
    {
      action( target );
    }
  }

  // Invoke the supplied function on the supplied 
  // lock object and return result:   

  public static T TryLock<TLock, T>( 
    this TLock target, 
    Func<TLock, T> func ) where TLock: class
  {
    CheckNull( target );
    if( isLockingEnabled )
    {
      lock( target )
      {
        return func( target );
      }
    }
    else
    {
      return func( target );
    }
  }

  // Invoke the supplied function on the supplied lock object 
  // and another supplied argument, and return the result:    

  public static T TryLock<TLock, TArg, T>( 
    this TLock target, 
    Func<TLock, TArg, T> func, 
    TArg arg ) where TLock: class
  {
    CheckNull( target );
    if( isLockingEnabled )
    {
      lock( target )
      {
        return func( target, arg );
      }
    }
    else
    {
      return func( target, arg );
    }
  }

  // Invoke the supplied action on the supplied lock object 
  // and another supplied argument:   

  public static void TryLock<TLock, TArg>( 
    this TLock target, 
    Action<TLock, TArg> func, 
    TArg arg )  where TLock: class
  {
    CheckNull( target );
    if( isLockingEnabled )
    {
      lock( target )
      {
        func( target, arg );
      }
    }
    else
    {
      func( target, arg );
    }
  } 
}

///// Example:

public static class SharedList<T>
{
  private static List<T> items = new List<T>();

  public static bool Remove( T item )
  {
    return items.TryLock( (list, item) => list.Remove( item ), item );
  }

  public static T GetItemAt( int index )
  {
    return items.TryLock( (list, i) => list[i], index );
  }

  public static bool Contains( T item )
  {
    return items.TryLock( (list, it) => list.Contains( it ), item );
  }

  public static void Add( T item )
  {
    items.TryLock( (list, item) => list.Add( item ) );
  }
}

} // namespace

Upvotes: 2

Marc Gravell
Marc Gravell

Reputation: 1063619

We can test it...

I get:

1000000000; 2164 (no lock)
1000000000; 23258 (lock)
21.094ns per lock

Code:

using System;
using System.Diagnostics;

static class P
{
    static void Main()
    {

        Test(1); // for JIT
        Test(1000000);
    }
    static readonly object syncLock = new object();
    static void Test(int count)
    {
        int j = 0;
        var watch = Stopwatch.StartNew();
        for(int i = 0 ; i < count ; i++)
        {
            for (int z = 0; z < 1000; z++)
                j++;
        }
        watch.Stop();
        long withoutMillis = watch.ElapsedMilliseconds;
        Console.WriteLine("{0}; {1} (no lock)", j, watch.ElapsedMilliseconds);

        j = 0;
        watch = Stopwatch.StartNew();
        for (int i = 0; i < count; i++)
        {
            for (int z = 0; z < 1000; z++ )
                lock (syncLock)
                {
                    j++;
                }
        }
        watch.Stop();
        long withMillis = watch.ElapsedMilliseconds;
        Console.WriteLine("{0}; {1} (lock)", j, watch.ElapsedMilliseconds);

        long deltaNano = (withMillis - withoutMillis) * 1000000;
                // nano = 1000 micro = 1000000 milli
        double perLockNano = deltaNano/(1000.0 * count);
        Console.WriteLine("{0}ns per lock", perLockNano);
    }
}

Upvotes: 25

Tudor
Tudor

Reputation: 62459

According to this source, the overhead to lock and unlock is about 20ns.

Upvotes: 10

Related Questions