Async re-entrancy, and the patterns to deal with it
What should we do in case of re-entrancy? For example, if the user clicks the button twice in rapid succession then this code will give this the wrong answer:
async void ButtonClick( object s, EventArgs e)
{
await FooAsync();
}
async Task FooAsync()
{
var x = ReadEntry();
await Task .Delay(100);
WriteEntry(x + 1);
}
There are several patterns to solve this problem, depending on what user-experience you want. These are just patterns that I've encountered in my own code. If people have criticisms or suggestions, I'd love to hear them.
Note: be sure to use Try/Finally, to make sure the button/field/semaphore is enabled/reset/released at the end of the method!
Re-entrancy pattern 1: temporarily disable the UI
In this pattern the very possibility of re-entrancy is eliminated at the UI itself:
async void ButtonClick( object s, EventArgs e)
{
Button1.IsEnabled = false ;
await FooAsync();
Button1.IsEnabled = true ;
}
async Task FooAsync()
{
...
}
Re-entrancy pattern 2: no-op if the method is called while already in flight
In this pattern, any re-entrant calls to the library method simply do nothing:
bool _inFoo = false ;
async Task FooAsync()
{
// Assumes being called on UI thread...
// if not, need atomicity here.
if (_inFoo) return ;
_inFoo = true ;
...
_inFoo = false ;
}
Re-entrancy pattern 3: serialize calls
In this pattern, calls to the library method are serialized.
SemaphoreSlim _inFoo = new SemaphoreSlim (1);
async Task FooAsync()
{
await _inFoo.WaitAsync();
...
_inFoo.Release();
}
Re-entrancy pattern 4: singleton method instance
In this pattern, iif an instance of the library method is already in-flight, then calls to the library method simply return that existing in-flight instance. The chief use of this is for a "lazy async initialization"… the first time you call the method, it kicks off the work, and subsequent calls will return the original task.
(This is easier in VB thanks to the compiler support for "static" local variables, which are shared over all invocations to the method, and have thread-safe initialization. The C# equivalent is fussier.)
Function FooAsync() As Task
Static t As Task = FooAsyncHelper()
Return t
End Function
Async Function FooAsyncHelper() As Task
...
End Function
Re-entrancy pattern 5: cancel previous invocation
In this pattern, if an instance of the library method is already in-flight, then calls to the library method will cancel the previous one, wait until its cancellation is complete, and then start a new one.
async Task Button1Click()
{
// Assume we're being called on UI thread... if not, the two assignments must be made atomic.
// Note: we factor out "FooHelperAsync" to avoid an await between the two assignments.
// without an intervening await.
if (FooAsyncCancellation != null ) FooAsyncCancellation.Cancel();
FooAsyncCancellation = new CancellationTokenSource ();
FooAsyncTask = FooHelperAsync(FooAsyncCancellation.Token);
await FooAsyncTask;
}
Task FooAsyncTask;
CancellationTokenSource FooAsyncCancellation;
async Task FooHelperAsync( CancellationToken cancel)
{
try { if (FooAsyncTask != null ) await FooAsyncTask; }
catch ( OperationCanceledException ) { }
cancel.ThrowIfCancellationRequested();
await FooAsync(cancel);
}
async Task FooAsync( CancellationToken cancel)
{
...
}
Comments
Anonymous
March 03, 2014
Good stuff, thanks. Regarding pattern #5, I prefer to wait for the previous task to end, after requesting the cancellation: stackoverflow.com/.../1768303Anonymous
March 03, 2014
@Noseratio, this pattern #5 actually DOES wait for the previous task to end. If you want to wait for it to end in its own time (without hastening that by requesting its cancellation), then simply avoid passing on the "cancel" argument to FooAsync.Anonymous
March 03, 2014
@Lucian Wischik, you're right, I've overlookedawait FooAsyncTask
.Anonymous
March 04, 2014
If you want to wait, it's better to use Pattern #3, or there is something I didn't see?Anonymous
March 05, 2014
@Felipe, yes I think #3 is a great way to await.Anonymous
March 16, 2014
I've used [code] Dim _task As Task Async Sub Blah() If (_task IsNot Nothing) AndAlso (Not _task.IsCompleted) Then Await _task _task = Task.Start(...) End Sub [/code] As it allowed the user to continue (in this case answering questions) whilst the task finished it job of saving previous answers back to the database.Anonymous
March 22, 2015
So if I have 2 I/o bound operations one is a upload and one is download(fetch). The does task parallel lib allows me to run both at same time. Parallel api's does the Task scheduler scheduling as you had explained, but can It run asynchronously parallel in real-time with 2 threads starting at same time. Can you provide some thought on this topic.