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)
{
...
}