Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reentrancy in async/await?

I have a button which has an async handler which calls awaits on an async method. Here's how it looks like:

private async void Button1_OnClick(object sender, RoutedEventArgs e)
{
    await IpChangedReactor.UpdateIps();
}

Here's how IpChangedReactor.UpdateIps() looks:

public async Task UpdateIps()
{
    await UpdateCurrentIp();
    await UpdateUserIps();
}

It's async all the way down.
Now I have a DispatcherTimer which repeatedly calls await IpChangedReactor.UpdateIps in its tick event.

Let's say I clicked the button. Now the event handler awaits on UpdateIps and returns to caller, this means that WPF will continue doing other things. In the meantime, if the timer fired, it would again call UpdateIps and now both methods will run simultaneously. So the way I see it is that it's similar to using 2 threads. Can race conditions happen? (A part of me says no, because it's all running in the same thread. But it's confusing)

I know that async methods doesn't necessarily run on separate threads. However, on this case, it's pretty confusing.

If I used synchronous methods here, it would have worked as expected. The timer tick event will run only after the first call completed.

Can someone enlighten me?

like image 796
wingerse Avatar asked Feb 15 '16 21:02

wingerse


People also ask

What is Reentrancy C#?

The term you are struggling to find is "Reentrancy": "In computing, a computer program or subroutine is called reentrant if it can be interrupted in the middle of its execution and then safely called again ("re-entered") before its previous invocations complete execution."

Is c# lock reentrant?

C#'s lock statement is built around the Monitor synchronization primitive. A Monitor is mutually exclusive, except that lock acquisition is reentrant. This means that if a thread already posses a Monitor lock and attempts to reacquire it, that the lock will be immediately acquired.


2 Answers

Since both calls run on the UI thread the code is "thread safe" in the traditional sense of - there wouldn't be any exceptions or corrupted data.

However, can there be logical race conditions? Sure. You could easily have this flow (or any other):

UpdateCurrentIp() - button
UpdateCurrentIp() - Timer
UpdateUserIps() - Timer
UpdateUserIps() - button

By the method names it seems not to really be an issue but that depends on the actual implementation of these methods.

Generally you can avoid these problems by synchronizing calls using a SemaphoreSlim, or an AsyncLock (How to protect resources that may be used in a multi-threaded or async environment?):

using (await _asyncLock.LockAsync())
{
    await IpChangedReactor.UpdateIps();
}

In this case though, it seems that simply avoiding starting a new update when one is currently running is good enough:

if (_isUpdating) return;

_isUpdating = true;
try
{
    await IpChangedReactor.UpdateIps();
}
finally
{
    _isUpdating = false;
}
like image 73
i3arnon Avatar answered Oct 23 '22 16:10

i3arnon


I can think of a number of ways to handle this issue

1 Do not handle it

Like i3arnon says it might not be a problem to have multiple calls to the methods running at the same time. It all depends on the implementation of the update methods. Just like you write, it's very much the same problem that you face in real, multi-threaded concurrency. If having multiple async operations running at once is not a problem for these methods, you can ignore the reentrancy issues.

2 Block the timer, and wait for running tasks to finish

You can disable the timer, och block the calls to the event handler when you know you have a async task running. You can use a simple state field, or any kind of locking/signaling primitive for this. This makes sure you only have a single operation running at a given time.

3 Cancel any ongoing async operations

If you want to cancel any async operations already running, you can use a cancellationtoken to stop them, and then start a new operation. This is described in this link How to cancel a Task in await?

This would make sense if the operation takes a long time to finish, and you want to avoid spending time to complete an operation that is already "obsolete".

4 Queue the requests

If it's important to actually run all the updates, and you need synchronization you can queue the tasks, and work them off one by one. Consider adding some sort of backpressure-handling if you go down this route...

like image 42
Mikael Nitell Avatar answered Oct 23 '22 16:10

Mikael Nitell