Zach
Zach

Reputation: 5885

How to refactor 'lock' to 'async/await'?

We have an old library written in C# targeting framework 2.0. Recently we are going to use it in a modern .net core project and intend to use async/await. However, the old library has a lot of lock blocks.

We plan to add new async methods to implement the same logic.

For example,

the old code

void GetOrder()
{
    // ...
    lock(_lock)
    {
    //...
    }
}

expected result

async Task AsyncGetOrder()
{
    // ...
    await DoSomethingWithLock()
}

Please give me some advices about how to translate lock into async/await.

Upvotes: 3

Views: 4311

Answers (3)

Matt Thomas
Matt Thomas

Reputation: 5734

There are a few general approaches:

#1 Refactor away from the need for locks at all

Look around for how to write lock-free C#. Sometimes it involved judicious use of the Interlocked class. Other times it involves a shift in mindset toward immutable state (ask a functional programmer). There are many cases when doing this has boosted parallel performance significantly.

And of course lockless threadsafe code can be executed either synchronously or asynchronously.

#2 Refactor away from the need for a reentrant lock, then go async with something that doesn't allow reentrance

This is basically what people are recommending when they recommend SemaphoreSlim, the AsyncEx NuGet package, or similar.

Stephen Cleary has written about the wisdom behind going this route. He also gives some examples of how to do it:

https://blog.stephencleary.com/2013/04/recursive-re-entrant-locks.html

#3 Find an async drop-in replacement for Monitor.Lock/lock

Basically what you need is something that gives all three of these at the same time:

  • Asynchronicity
  • Reentrance
  • Mutual exclusion

Monitor.Lock/lock give you the second and third things. So you want something that gives you the first one also without sacrificing the other two.

This is a little trickier than it might seem. At first glance there are several NuGet packages which appear to do this. But the only correct one that I know of is this NuGet package (which I wrote):

https://www.nuget.org/packages/ReentrantAsyncLock/

Here it is in action:

var asyncLock = new ReentrantAsyncLock();
var raceCondition = 0;
// You can acquire the lock asynchronously
await using (await asyncLock.LockAsync(CancellationToken.None))
{
    await Task.WhenAll(
        Task.Run(async () =>
        {
            // The lock is reentrant
            await using (await asyncLock.LockAsync(CancellationToken.None))
            {
                // The lock provides mutual exclusion
                raceCondition++;
            }
        }),
        Task.Run(async () =>
        {
            await using (await asyncLock.LockAsync(CancellationToken.None))
            {
                raceCondition++;
            }
        })
    );
}
Assert.Equal(2, raceCondition);

This is certainly not the first attempt at doing this. But like I said it's the only correct attempt that I've seen so far. Some other implementations will deadlock trying to re-enter the lock in one of the Task.Run calls. Others will not actually provide mutual exclusion and the raceCondition variable will sometimes equal 1 instead of 2:

Upvotes: 9

asaf92
asaf92

Reputation: 1855

The first thing you need to take into account is that async methods can call non-async methods, but it's not trivial for non-async methods to call async methods and wait for them to finish.

This means that every method that has a lock inside of it will probably need to be called only by async methods. You can do blocking waits for async methods but then there's no point to refactoring and you have to be very careful to avoid deadlocks.

You also need to be aware that in some project types there's an importance to the identity of the executing thread. For example in WPF there are some things that only the UI thread is allowed to do, and if you launch a Task that runs such code from a thread in the thread-pool, you're likely to experience exceptions.

Having said that, if you want to refactor a method that waits on a lock into an async method that asynchronously waits for a lock/semaphore, you should use SemaphoreSlim and await the WaitAsync call. This way the async method will yield control when the SemaphoreSlim is blocking execution, and resume execution at some point later.

Upvotes: 2

Etienne Charland
Etienne Charland

Reputation: 4024

You could use SemaphoreSlim, but if there's a lot of it, the AsyncLock library will probably make the conversion much easier (and cleaner).

Just go with the AsyncLock library and relax.

Upvotes: 3

Related Questions