59

I saw the following code, and wanted to use it for a simple activity which may only be executed one at a time, and won't occur frequently (so the chance of occurring twice at a time is very small, but you never know).

So the code:

// class variable
private static object syncRoot = new object();

// in a method:
lock (syncRoot)
{
    DoIt();
}

When another thread comes by and wants to execute the code, how long will it wait until the lock is released? Forever, or can you somehow set a timeout?

And second: if the DoIt() method throws an exception, is the lock still released?

SharpC
  • 6,974
  • 4
  • 45
  • 40
Michel
  • 23,085
  • 46
  • 152
  • 242

7 Answers7

102

When another thread comes by and wants to execute the code, how long will it wait until the lock is released?

lock will block the the thread trying to enter the lock indefinitely until the object being locked on is released.

can you somehow set a timeout?

If you need to specify a timeout, use Monitor.TryEnter as in

if(Monitor.TryEnter(obj, new TimeSpan(0, 0, 1))) {
    try {
        body 
    }
    finally {
        Monitor.Exit(obj);
    }
}

if the DoIt() method throws an exception, is the lock still released?

Yes, a lock(obj) { body } is translated to:

bool lockWasTaken = false;
var temp = obj;
try { Monitor.Enter(temp, ref lockWasTaken); { body } }
finally { if (lockWasTaken) Monitor.Exit(temp); }

For the gory details on what can happen when an exception is thrown, see Locks and exceptions do not mix.

djk
  • 943
  • 2
  • 9
  • 27
jason
  • 236,483
  • 35
  • 423
  • 525
  • For a semi-automatic implementation, you can check http://www.informit.com/articles/article.aspx?p=1231461 and http://www.interact-sw.co.uk/iangblog/2004/03/23/locking (using the using/dispose to automate some of the typing). – Andreas Reiff Dec 30 '13 at 21:32
  • 9
    **A fair warning!** The using-statement isn't 100% safe (many tend to think it is!): The using-statement will only call Dispose _if it received the IDisposable-reference_. This happens shortly _after_ the statement inside the using-parentheses, i.e. `using (/*this stuff*/)`, completes. Since there is absolutely no requirement for atomicity (that statement can be arbitrarily long) something can happen there that prevents the (compiler-generated) using-code from ever receiving your clever IDisposable reference. _(tl;dr: keep using-statements simple - put the complicated stuff inside its body.)_ – AnorZaken Feb 27 '15 at 11:14
  • The link to "Locks and exceptions do not mix" is broken. – Steve Smith May 11 '21 at 13:23
41

As mentioned, a regular lock will wait forever, which is a risk of deadlocks.

The preferred mechanism is (and note the ref):

bool lockTaken = false;
try {
    Monitor.TryEnter(lockObj, timeout, ref lockTaken);
    if(!lockTaken) throw new TimeoutException(); // or compensate
    // work here...
} finally {
    if(lockTaken) Monitor.Exit(lockObj);
}

This avoids the risk of not releasing the lock in some edge-cases.

The finally (which exists in any sensible implementation) ensures the lock is released even in error conditions.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • 2
    I want to add that a lot of programmers add that finally statement as a _habit_, but a habit it shouldn't be: "Releasing the lock in error conditions" is not auto-magically better than a dead-lock. It is something that should be evaluated on a case-to-case basis. In a safety-critical scenario releasing the lock in a state of error could be dangerous - detecting a dead-locked process and performing some sort of recovery procedure can _sometimes_ be safer than releasing the lock. "Sometimes" being the operative word. – AnorZaken Jun 29 '15 at 20:24
14

A simple lock(syncRoot) will wait forever.

You can replace it with

if (System.Threading.Monitor.TryEnter(syncRoot, 1000))
{
     try
     {
         DoIt();
     }
     finally
     {
         System.Threading.Monitor.Exit(syncRoot);
     }
}

Every thread should ensure exception-safe locking.

Note that the standard lock(syncRoot) {} is rewritten to Monitor.Enter(syncRoot) and a try/finally

H H
  • 263,252
  • 30
  • 330
  • 514
3

The lock will wait forever, as Henk said. An exception will still unlock. It is implemented internally with a try-finally block.

Mr47
  • 2,655
  • 1
  • 19
  • 25
2

You should take a step back and ask yourself why you are letting an exception from a method have any say over when a Monitor you Enter is Exited. If there's even a chance that DoIt() might throw an exception (and I would argue that if possible you re-write DoIt() so that it DoesNot), then you should have a try/catch block within the lock() statement so that you can ensure you perform any necessary clean-up.

dlev
  • 48,024
  • 5
  • 125
  • 132
1

Jason anwser is excellent, here is a way to wrap it making the call simpler.

    private void LockWithTimeout(object p_oLock, int p_iTimeout, Action p_aAction)
    {
        Exception eLockException = null;
        bool bLockWasTaken = false;
        try
        {
            Monitor.TryEnter(p_oLock, p_iTimeout, ref bLockWasTaken);
            if (bLockWasTaken)
                p_aAction();
            else
                throw new Exception("Timeout exceeded, unable to lock.");
        }
        catch (Exception ex)
        {
            // conserver l'exception
            eLockException = ex;
        }
        finally 
        { 
            // release le lock
            if (bLockWasTaken) 
                Monitor.Exit(p_oLock); 

            // relancer l'exception
            if (eLockException != null)
                throw new Exception("An exception occured during the lock proces.", eLockException);
        }
    }

and then, use it this way:

        // ajouter à la liste des fiches à loader
        LockWithTimeout(m_lLoadingQueue, 3600, () =>
        {
            m_lLoadingQueue.Add(p_efEcranFiche);
        });
Sam L.
  • 207
  • 2
  • 3
0

Know the necessary preconditions for deadlock to take place. Always use Monitor vs Lock to avoid deadlocks.

Conditions

Petter Friberg
  • 21,252
  • 9
  • 60
  • 109