6

I have read: http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html and the accepted answer at deadlock even after using ConfigureAwait(false) in Asp.Net flow but am just too dense to see what is going on.

I have code:

private void CancelCalibration()
{
    // ...
    TaskResult closeDoorResult =  CloseLoadDoor().ConfigureAwait(false).GetAwaiter().GetResult(); 
    CalibrationState = CalibrationState.Idle;

    return;
    // ...                   
}

private async Task<TaskResult> CloseLoadDoor()
{       
    TaskResult result = await _model.CloseLoadDoor().ConfigureAwait(false);           
    return result;
}
public async Task<TaskResult> CloseLoadDoor()
    {
        TaskResult result = new TaskResult()
        {
            Explanation = "",
            Success = true
        };
        await _robotController.CloseLoadDoors().ConfigureAwait(false);
        return result;
    }
    public async Task CloseLoadDoors()
    {                         
            await Task.Run(() => _robot.CloseLoadDoors());              
    }

     public void CloseLoadDoors()
    {
   // syncronous code from here down              
   _doorController.CloseLoadDoors(_operationsManager.GetLoadDoorCalibration());                
        }

As you can see, CloseLoadDoor is declared async. I thought (especially from the first article above) that if I use ConfigureAwait(false) I could call an async method without a deadlock. But that is what I appear to get. The call to "CloseLoadDoor().ConfigureAwait(false).GetAwaiter().GetResult() never returns!

I'm using the GetAwaiter.GetResult because CancelCalibration is NOT an async method. It's a button handler defined via an MVVM pattern:

public ICommand CancelCalibrationCommand
        => _cancelCalibrationCommand ?? (_cancelCalibrationCommand = new DelegateCommand(CancelCalibration));

If someone is going to tell me that I can make CancelCalibration async, please tell me how. Can I just add async to the method declaration? HOWEVER, I'd still like to know why the ConfigureAwait.GetAwaiter.GetResult pattern is giving me trouble. My understanding was that GetAwaiter.GetResult was a way to call async method from syncronous methods when changing the signature is not an option.

I'm guessing I'm not really freeing myself from using the original context, but what am I doing wrong and what is the pattern to fix it? Thanks, Dave

Dave
  • 8,095
  • 14
  • 56
  • 99
  • 1
    I don't get a deadlock with the shown code (emulating `_model.CloseLoadDoor()` with `Task.Delay()`). – GSerg Jan 25 '19 at 21:02
  • 1
    If you're using `CancelCalibration` as the delegate to command that it's a logical event handler and can be marked `async void` which would allow you to `await CloseDoor()` – JSteward Jan 25 '19 at 21:06
  • 2
    _This exception includes methods that are logically event handlers even if they’re not literally event handlers (for example, ICommand.Execute implementations)_ [Async/Await Best Practices](https://msdn.microsoft.com/en-us/magazine/jj991977.aspx) – JSteward Jan 25 '19 at 21:12
  • There is a pattern of sorts, don't write code that you cannot debug. The usual problem with async, you don't know whether to blame the hammer of the nail. – Hans Passant Jan 25 '19 at 23:33
  • Thanks GSerg. I think _model.CloseLoadDoor will result in returning right away. Perhaps that is the right thing to do here in this example, but I'd like to know how to wait for the door to be closed from a function that is not marked async. Thanks! – Dave Jan 26 '19 at 15:49
  • JSteward, In fact, marking CancelCalibration async and calling await _model.CloseDoor() is exactly what I ended up doing and seems to work. BUT I'd like to know what is going on with ConfigureAwait and GetAwaiter. Perhaps there will be a time when I can't mark a method async? I must admit, I'm having trouble thinking of such a case :). But understanding ConfigureAwait and GetAwaiter seems like a noble goal in any case! Thank you! – Dave Jan 26 '19 at 15:51

1 Answers1

11

I thought (especially from the first article above) that if I use ConfigureAwait(false) I could call an async method without a deadlock.

There's an important note in that article:

Using ConfigureAwait(false) to avoid deadlocks is a dangerous practice. You would have to use ConfigureAwait(false) for every await in the transitive closure of all methods called by the blocking code, including all third- and second-party code. Using ConfigureAwait(false) to avoid deadlock is at best just a hack).

So, is ConfigureAwait(false) used for every await in the transitive closure? This means:

  • Does CloseLoadDoor use ConfigureAwait(false) for every await? We can see from the code posted that it does.
  • Does _model.CloseLoadDoor use ConfigureAwait(false) for every await? That we cannot see.
  • Does every method called by _model.CloseLoadDoor use ConfigureAwait(false) for every await?
  • Does every method called by every method called by _model.CloseLoadDoor use ConfigureAwait(false) for every await?
  • etc.

This is a severe maintenance burden at least. I suspect that somewhere down the call stack, there's a missing ConfigureAwait(false).

As that note concludes:

As the title of this post points out, the better solution is “Don’t block on async code”.

In other words, the whole point of that article is "Don't Block on Async Code". It's not saying "Block on Async Code with This One Neat Trick".

If you do want to have an API that supports both synchronous and asynchronous callers, I recommend using the bool argument hack in my article on brownfield async.


On a side note, in the code CloseLoadDoor().ConfigureAwait(false).GetAwaiter().GetResult(), the ConfigureAwait doesn't do anything. It's "configure await", not "configure task". Since there's no await there, the ConfigureAwait(false) has no effect.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thank you Stephen. I was hoping you'd examine this question. I edited my code to show the other methods called. They all use 'ConfigureAwait(false)' except when we switch over to syncronous code and use await Task.Run(() => _robot.CloseLoadDoors()); WITHOUT a ConfigureAwait. Could this be the problem? – Dave Jan 27 '19 at 22:22
  • Stephen, I'm confused by your "On a side note" comment. Perhaps I'm using ConfigureAwait().GetAwaiter().GetResult() incorrectly? My intention was to wait for the results of CloseLoadDoor. Perhaps I'm not doing that? Do I need a wait in this call? Or another way to ask the quesiton... If I told you you couldn't use 'await' for some strange reason, how would you write the call to CloseLoadDoor().ConfigureAwait(false).GetAwaiter().GetResult() correctly to mimic using await? I don't understand your comment about: It's configure await, not configure task". Thanks! – Dave Jan 27 '19 at 22:25
  • I reread your article: https://msdn.microsoft.com/en-us/magazine/mt238404.aspx?f=255&MSPPError=-2147217396 It seems to me if I can't guarantee that ConfigureAwait(false) is used all the way down the stack, I'm safer using Task.Run(() => CloseLoadDoor().GetAwaiter().GetResult(); ! Of course in my situation I can change my top method to async and use await, but as your article points out, there may be times when that can't be done. Thank you! – Dave Jan 27 '19 at 22:48
  • 3
    @Dave: Yes, missing the `ConfigureAwait(false)` on `await Task.Run(...);` can certainly cause the deadlock here. The problem with `.ConfigureAwait(false).GetAwaiter().GetResult()` is that the `.ConfigureAwait(false)` is meaningless there; it's exactly the same thing as just `.GetAwaiter().GetResult()` without the `ConfigureAwait(false)`. There is no *perfect* solution for sync-over-async, but wrapping it in `Task.Run` and blocking on that will work for *most* code. – Stephen Cleary Jan 28 '19 at 00:52
  • Thanks Stephen. It's very clear to me now. It still strikes me as a very difficult problem in the real world because one cannot always guarantee that "await ... ConfigureAwait(false)" has been used all the way down. One may not even be able to see the code (hidden in library) or not be able to change it for some reason. I'm curious why you say wrapping it in "Task.Run and blocking on that will work for most code". Why "most code". Do you give examples where it fails in your articles? Thanks for all the attention. Answer marked as accepted and hopefully will help others! – Dave Jan 28 '19 at 16:51
  • I should have added that it's not a problem as long as I don't block. I believe that point has sunken in to my brain now! – Dave Jan 28 '19 at 17:01
  • @Dave: Yes, that's why I do *not* recommend using `ConfigureAwait(false)` to allow blocking. I have seen one scenario where even Microsoft code missed one, and then what do you do? Regarding code that is not "most code", I mean e.g., some code that depends on `HttpContext.Current`, or attempts to access UI components, or [just has an implicit dependency that all continuations are serialized because the author only tested in a single-threaded context but `Task.Run` is a free-threaded context](https://blog.stephencleary.com/2017/03/aspnetcore-synchronization-context.html). – Stephen Cleary Jan 28 '19 at 17:48