Reputation: 5885
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
Reputation: 5734
There are a few general approaches:
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.
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
Monitor.Lock
/lock
Basically what you need is something that gives all three of these at the same time:
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
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
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