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 (7)

  1. Noseratio says:

    Good stuff, thanks. Regarding pattern #5, I prefer to wait for the previous task to end, after requesting the cancellation: stackoverflow.com/…/1768303

  2. ljw1004 says:

    @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.

  3. Noseratio says:

    @Lucian Wischik, you're right, I've overlooked `await  FooAsyncTask`.

  4. Fujiy says:

    If you want to wait, it's better to use Pattern #3, or there is something I didn't see?

  5. ljw1004 says:

    @Felipe, yes I think #3 is a great way to await.

  6. Adam Speight says:

    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.

  7. Varun says:

    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.