Lock and Async method in C#
Asked Answered
B

5

86

I am not clear (and can't find documentation clear enough): when using the lock keyword in an async method: will the thread be blocked if the object is already blocked or will it return a task in suspended state (not blocking the thread, and returning when the lock is released)?

In the code below, will the line block the thread? If it blocks the thread (which is what I think), is there an standard not blocking solution? I am considering using AsyncLock, but first I wanted to try for something standard.

private object myLock = new object(); 

private async Task MyMethod1()
{
    lock (myLock) // <---- will this line cause a return of the current method
                  // as an Await method call would do if myLock was already locked? 
    {
        //.... 
    }
}

// other methods that lock on myLock
Bedivere answered 19/11, 2013 at 23:51 Comment(2)
Take a look here for a good explanation #7613102Dewittdewlap
Does this answer your question? Why can't I use the 'await' operator within the body of a lock statement?Volta
C
23

No it won't.

lock is syntactic sugar for Monitor.Enter and Monitor.Exit. lock will keep execution in the method until the lock is released. It does not function like await in any way, shape or form.

Carcinogen answered 19/11, 2013 at 23:54 Comment(3)
Thanks. BTW: I guess applying to my question: "Yes, the thread will block", is the answer that I am looking for.Bedivere
lock() spins for a short period and then blocks. If it just kept spinning it'd change inefficient code into inefficient time-and-power-eating code.Meperidine
A bad use of the word "spin" on my part. I just meant to say that execution does not leave the method like it does with await. I will rephrase. Thanks @CoryNelson.Carcinogen
S
133

This has been disallowed to stop deadlocks (i.e. developers hurting themselves). The best solution I've found is to use semaphores - See this post for details.

Relevant code extract:

static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);

...

await semaphoreSlim.WaitAsync();
try
{
    await Task.Delay(1000);
}
finally
{
    semaphoreSlim.Release();
}
Strudel answered 19/8, 2017 at 7:57 Comment(9)
Is there a reason for making the semaphore static? presumably it would work just as well if the semaphore were an instance variable (assuming you wanted to only lock this instance of the class)Xanthophyll
In my case, I have multiple objects that need to report their progress to a database. I want them to be async of course, so a static SemaphoreSlim in my case is the way to go.Wraith
Thanks for the answer. The use of SemaphoreSlim, solved my problem.Placatory
Regarding Static: on a webserver, if you have an singleton that uses async... Each Request against the server is usually executed in its own thread - so if you have 3-4 almost concurrent calls they might try accessing the object simultaniously.Nobe
I forgot to say, that the only way to share a semaphore across threads on a webserver, is to use a Static Semaphore.Nobe
@MartinKirk, avoid static at any cost. Use services.RegisterSingleton() and let DI container to do it's job.Petasus
@Petasus LOL - the whole reason why we are using Semaphore in a Singleton is because SINGLETONS ARE NOT THREADSAFE ! And the only way to share the semaphore across threads are by having it be StaticNobe
@MartinKirk, LOL on you : services.RegisterSingleton(new Semaphore(1,1)).AsNamed("lockName") and inject it into any service type(s) that needs synchronization. Use Autofac that supports named/index servicesPetasus
WaitAsync should be in the try blockMarque
A
92

In the code below, will the line block the thread?

Technically, yes, but it won't work as you expect.

There are two reasons why thread-affine locks don't play well with async. One is that (in the general case), an async method may not resume on the same thread, so it would try to release a lock it doesn't own while the other thread holds the lock forever. The other reason is that during an await while holding a lock, arbitrary code may execute while the lock is held.

For this reason, the compiler goes out of its way to disallow await expressions within lock blocks. You can still shoot yourself in the foot by using Monitor or other primitives directly, though.

If it blocks the thread (which is what I think), is there an standard not blocking solution?

Yes; the SemaphoreSlim type supports WaitAsync.

Astrahan answered 20/11, 2013 at 0:45 Comment(10)
Thanks now that I see the problem doing await in lock. What about the opposite: using locks in a method that's called eventually by some async method? My guess is that it's safe, as the locked operation are all completed on the same thread. However, using Monitors directly seems still problematic in this case - but that sucks, because there is usually no way the dev can tell if the code is going to be used in a async/await fashion or not. What's the suggestion?Ammonic
As long as there's only synchronous code in the lock (or monitor), then it'll work fine. It doesn't matter whether it's called by an asynchronous method or not.Astrahan
I thought this is what the question was asking (Is a lock inside an async method safe). Thankfully I realized it was a little unclear and scrolled down to these comments. Hahaha.Out
"why thread-affine locks don't play well with async" - I can't wrap my head around that: shouldn't the following always happen? (1) thread 1 reaches lock, acquires it (2) thread 1 reaches await, let go of execution (3) thread 2 reaches lock, blocks on it until released (that's bad in an async function, but that's another issue) (4) thread 1 execution continues (awaiting finished), releases lock, happily continues (5) thread 2 acquires lock, happily continues.Ase
@OfirD: There's one major problem: the async method may resume executing on a different thread after the await, leading to a situation where a thread releases a lock it never acquired.Astrahan
Huh, that's already in your answer, but only now I got it :) But what about the other reason you give: "during an await while holding a lock, arbitrary code may execute while the lock is held" - continuing my last example, I'd say that thread 2 is the "arbitrary code [being] execute[d] while the lock is held" by thread 1. What can go wrong with that?Ase
@OfirD: Now consider multiple async methods run on the same thread 1: function A acquires the lock and does the await. Then function B runs on the same thread; if it acquires the same lock, it will succeed instead of blocking. Also, in the more general case, function B can depend on some code run by thread 2 that acquires the lock - in that case you end up with a deadlock because thread 1 is holding the lock, waiting for thread 2, which is waiting for thread 1 to release the lock.Astrahan
Thanks so much! I'll drop here a link related to the first part of your last comment: Under what conditions can a thread enter a lock (Monitor) region more than once concurrently?Ase
What's the problem with "arbitrary code executing while the lock is held"? I've seen this in multiple places now and I really wish someone would give a full for-dummies explanation with a worked example because I'm not seeing the problem.Kristikristian
It's one of three multithreading pillars from an old Dr Dobbs article IIRC. In the general case, executing arbitrary code while holding a lock may cause a deadlock, e.g., if the arbitrary code chooses to depend on some other thread taking that same lock.Astrahan
C
23

No it won't.

lock is syntactic sugar for Monitor.Enter and Monitor.Exit. lock will keep execution in the method until the lock is released. It does not function like await in any way, shape or form.

Carcinogen answered 19/11, 2013 at 23:54 Comment(3)
Thanks. BTW: I guess applying to my question: "Yes, the thread will block", is the answer that I am looking for.Bedivere
lock() spins for a short period and then blocks. If it just kept spinning it'd change inefficient code into inefficient time-and-power-eating code.Meperidine
A bad use of the word "spin" on my part. I just meant to say that execution does not leave the method like it does with await. I will rephrase. Thanks @CoryNelson.Carcinogen
G
1

You task will not return in suspended state. It will wait until myLock is unlocked to run the code within lock statement. It will happen no matter what C# asynchronous model you use.

In other words, no two threads will be able to run statements inside the lock. Unless, there are many different instances of myLock object.

Gagliardi answered 20/11, 2013 at 0:5 Comment(0)
C
1

Can't tell what was in 2013, but in 2024 and C# 12 it is perfectly OK to lock() in an async function as far as you don't await inside it.

Without await, the code is executed in one thread and compiler is happy to lock().

Crimple answered 11/4 at 3:45 Comment(1)
The same was true in 2013 as well. Nothing changed since then.Minni

© 2022 - 2024 — McMap. All rights reserved.