0

I have a minimal example of some async code that is exhibiting strange behavior. This is sandbox code, more geared at trying to understand async better --

private async Task ExhibitStrangeBehaviorAsync()
{
    async Task TaskA()
    {
        await Task.Run(async () =>
        {
            throw new Exception(nameof(TaskA));
            await Task.Yield();
        });
    }

    async Task TaskB()
    {
        await Task.Run(() =>
        {
            throw new Exception(nameof(TaskB));
        });
    }

    var tasks = new List<Task>
    {
        TaskA(),
        TaskB(),
    };
    var tasksTask = Task.WhenAll(tasks);

    try
    {
        await tasksTask;
    }
    catch
    {
        Debug.WriteLine(tasksTask.Exception.Message);
    }
}

Intermittently, this code will hang. I would like to better understand why. My guess currently is the intermittent nature is due to the out-of-order execution of the aggregated tasks, and/or this line from Asynchronous Programming:

Lambda expressions in LINQ use deferred execution, meaning code could end up executing at a time when you're not expecting it to.

TaskA would fall in this category.

The code does not seem to hang if TaskB also Task.Runs an async lambda, or if neither local Task function contains Task.Run, e.g.

    async Task TaskA()
    {
        //await Task.Run(async () =>
        //{
             throw new Exception(nameof(TaskA));
             await Task.Yield();
        //});
    }

    async Task TaskB()
    {
        //await Task.Run(() =>
        //{
            throw new Exception(nameof(TaskB));
        //});
    }

Can anybody shed some light on what's going on here?

EDIT

This is executing in the context of a UI thread, specifically that of a Xamarin.Forms application.

EDIT 2

Here is another variant on it that runs straight out of the Xamarin.Forms OnAppearing lifecycle method. I had to modify TaskA/B slightly, though they break this way with the original setup above as well.

protected async override void OnAppearing()
{
    base.OnAppearing();

    async Task TaskA()
    {
        await Task.Run(async () =>
        {
            throw new InvalidOperationException();
            await Task.Delay(1).ConfigureAwait(false);
        }).ConfigureAwait(false);
    }

    async Task TaskB()
    {
        await Task.Run(() => throw new ArgumentException()).ConfigureAwait(false);
    }

    var tasks = new List<Task>
    {
        TaskA(),
        TaskB(),
    };
    var tasksTask = Task.WhenAll(tasks);

    try
    {
        await tasksTask;
    }
    catch
    {
        Debug.WriteLine(tasksTask.Exception.Message);
    }
}

There is some chance this may be related to another issue I have had - I am using an older version of Xamarin.Forms, one which has problems with its OnAppearing handling async correctly. I am going to try with a newer version to see if it resolves the issue.

Bondolin
  • 2,793
  • 7
  • 34
  • 62
  • 1
    How are you running `ExhibitStrangeBehaviorAsync`? Is this a console or a WPF/WinForms app? – canton7 Jul 15 '20 at 16:08
  • Works fine to me in the console. Where this code is used? – Guru Stron Jul 15 '20 at 16:09
  • 2
    There's nothing in there that should hang, and I can't reproduce it in a console app either. I'm guessing you're using a `.Wait()` further up the chain and this is a GUI app? But a *complete* reproducer would be good, so we're not guessing. If you are calling `ExhibitStrangeBehaviorAsync().Wait()`, there's a race which could cause a deadlock – canton7 Jul 15 '20 at 16:09

1 Answers1

0

I'm going to guess you're calling ExhibitStrangeBehaviorAsync().Wait() in a GUI app (or similar), as that's the only situation which I think of which can cause a deadlock here. This answer is written on that assumption.

The deadlock is this one, cause by the fact that you're running an await on a thread which has a SynchronizationContext installed on it, and then blocking that same thread higher up the callstack with a call to .Wait().

When you run TaskA() and TaskB(), both of those methods post some work to the ThreadPool, which takes a variable amount of time. When the ThreadPool gets around to actually executing the throw statements, this causes the Task returned from TaskA / TaskB to complete with an exception.

tasksTask will complete when the two tasks returned from TaskA and TaskB complete.

The race comes from the fact that, at the point that this line is executed:

await tasksTask;

the task tasksTask may or may not have completed. tasksTask will have completed if the Tasks returned from TaskA and TaskB have completed, so it's a race against the main thread's progression towards the await tasksTask line, against how fast the ThreadPool can run both of those throw statements.

If tasksTask is completed, the await happens synchronously (it's smart enough to check whether the Task being awaited has already completed), and there's no chance of a deadlock. If tasksTask hasn't completed, then my guess is that you're hitting the deadlock described here.

This is also consistent with your observation that removing the calls to Task.Run removes the deadlock. In this case, the tasks returned from TaskA and TaskB complete synchronously as well, so there's no race.


The morale of the story is, as is oft-repeated, do not mix async and sync code. Do not call .Wait() or .Result on a Task which is influenced in any way by await. Also consider tactical use of .ConfigureAwait(false) to guard against other people calling .Wait() on tasks which you produce.

canton7
  • 37,633
  • 3
  • 64
  • 77
  • Thanks for the detailed writeup. I would agree there is a race condition somewhere, and further tests have confirmed your diagnosis. Calling `Task.Delay` for a substantial amount of time in async `TaskA` and async `TaskB` causes the deadlock, whereas before having both functions async did not hang. However, I am not (directly, visibly) using .Wait. I know `WhenAll` is not blocking, but is there a chance it `Wait`s internally? I would find that hard to believe. – Bondolin Jul 15 '20 at 17:04
  • I just repeated the `Task.Delay` test and it did not deadlock the second time. – Bondolin Jul 15 '20 at 17:07
  • That was what the above was intended to be. But it's down in the guts of my app framework. Trying to put something together that's just straight out of `OnAppearing` and I'm not getting so far. I'll keep trying. – Bondolin Jul 15 '20 at 17:57
  • Yeah, something that we can run which reproduces the issue. If it only reproduces when bits of your application are present, then they're relevant to understanding what's happening – canton7 Jul 15 '20 at 17:58
  • I have posted a simpler version that still intermittently deadlocks. However, it seems this is just an issue with this version of Xamarin.Forms (4.2.0). At any rate, I can't seem to reproduce it on a newer version. – Bondolin Jul 15 '20 at 19:38
  • OK. Take a look at the Threads window when it deadlocks. Double click each thread, see where it's stuck. Also put `ConfigureAwait(false)` on your awaits, see if that changes anything – canton7 Jul 15 '20 at 19:44
  • After letting it sit and spin for awhile, I hit pause and looked at the threads window. I had one hanging out in what looks like general Android OS code, and two sitting on `System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw( Parameters)`, which I can only assume are from the exceptional tasks. – Bondolin Jul 15 '20 at 19:59
  • Adding `ConfigureAwait(false)` only adds the number of `Thread started: ` messages in the Output log. I am not very knowledgeable on how the thread pool works, but if it runs on a queue I wonder if the continuation is getting scheduled on a worker thread further down the queue that never gets executed? – Bondolin Jul 15 '20 at 20:01