Synchronization in async C# methods

Wednesday, August 29, 2012


The C# keyword async allows one to write synchronous-looking asynchronous code. While very convenient, sometimes there are non-obvious pitfalls.

Suppose in an async method, we want to await something while holding a lock. An example might be a critical section where two network requests should never be made concurrently. The following results in a compile-time error because you cannot use await inside a lock block:

lock (foo) {
    await Task.Delay(1);
}

(Task.Delay(n) is a Task that does nothing and completes after n milliseconds. I'm using it here as a convenient example of an asynchronous operation.)

You may think to use System.Threading.Mutex:

private Mutex mutex = new Mutex();
public async Task DoSomethingAsync() {
    mutex.WaitOne();
    await Task.Delay(1);
    mutex.ReleaseMutex();
}

The catch here is that many, but not all, awaited methods will resume execution on a different thread than the calling thread. In the example above, Task.Delay(n) resumes on a different thread whenever n > 0, so the thread that attempts to release the Mutex will be different than the one which acquired it. Because a Mutex must only be released by the same thread that acquired it, the following exception will be thrown:

System.ApplicationException: Object synchronization method was called from an unsynchronized block of code.

However, you can just use System.Threading.Semaphore to do the same thing, since these aren't bound to any particular thread:

private Semaphore semaphore = new Semaphore(1, 1);
public async Task DoSomethingAsync() {
    semaphore.WaitOne();
    await Task.Delay(1);
    semaphore.Release();
}

Of course, if you are holding synchronization objects for long periods of time as implied by the await keyword, it may be worth rethinking the design to avoid this issue and the inevitable blocking of threads during long asynchronous operations.

Tags: csharp, synchronization, mutex, async | Posted at 10:38 | Comments (5)


Comments

Henrik Cooke on Thursday, January 24, 2013 at 07:42

Using a semaphore, as in your example, can cause a task to lock "itself" out in more advanced scenarios. I'm currently looking for a solution to this problem, maybe you can think of one?

David on Thursday, January 24, 2013 at 20:07

The code above is pretty hacky and I don't recommend it. Perhaps if you post the specific issue you're having though, it would be easier to figure out what's going on? The simple code I listed above shouldn't ever deadlock.

Nirotpal on Wednesday, August 5, 2015 at 05:49

Does that mean that thread 1 acquires the lock then at this line --
await Task.Delay(1);

a new thread starts to execute it? and that new thread ( thread2) releases the lock? If that is true what happens to the previous thread thread1 lock?

David on Wednesday, August 5, 2015 at 13:15

@Nirotpal: Which example are you referring to? The first one (using the lock keyword) doesn't compile. The second one (using Mutex) will throw a runtime exception. The third one (using Semaphore) works because you can the down/up actions on semaphores can be performed across different threads, even concurrently.

A semaphore is not exactly the same as a mutex, but can be used as one if desired. In the third example, the semaphore is "acquired" on thread 1 and "released" on thread 2.

Hunter on Monday, April 23, 2018 at 09:30

Hi

Thanks for article.

To best of my knowledge, Asynchronous operations (awaited tasks) do not run on a separate thread (there is no thread), and unless ur using configure.await false, then async operations return to the main thread

Thanks

Add a comment

Name:
Email: (optional, not displayed to public)
URL:

Comment: