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/.../1768303

  • Anonymous
    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 overlooked await  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.