adamcunnington
adamcunnington

Reputation: 382

Code Does Not "Finish" in system.Threading.Tasks.Task Method

I call a system.Threading.Tasks.Task method with .Wait() from Main. The method has a return statement at the end which I would hope denotes that the task has "finished", allowing Main to continue execution. However, the return statement is hit (debugged with breakpoint) but execution does not continue in Main but instead seems to just "hang", with no further execution happening.

Code is here.

using System;
using System.IO;

using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Auth.OAuth2.Responses;
using Google.Apis.Dfareporting.v1_3;
using Google.Apis.Dfareporting.v1_3.Data;
using _file = Google.Apis.Dfareporting.v1_3.Data.File;
using Google.Apis.Download;
using Google.Apis.Services;
using Google.Apis.Util.Store;

namespace DCMReportRetriever
{
    public class DCMReportRetriever
    {
        public static void Main()
        {
            new DCMReportRetriever().Run().Wait();
            return; // This statement is never executed
        }

        private async System.Threading.Tasks.Task Run()
        {

            ...

            foreach (_file f in files)
            {
                if (f.Status == "REPORT_AVAILABLE" && f.LastModifiedTime >= startDateSinceEpoch)
                {
                    using (var stream = new FileStream(f.FileName + ".csv", FileMode.Append))
                    {
                        new MediaDownloader(service).Download(f.Urls.ApiUrl, stream);
                    }
                }
            }
            return; // This statement is hit (debugged with breakpoint)
        }
    }
}

Edit: I should add that Main is my entry point and apparently static async void Main is bad?

Upvotes: 2

Views: 3481

Answers (1)

VMAtm
VMAtm

Reputation: 28355

This is a common mistake with deadlock via context switching by async/await state machine.

You can find a clearly explained here:

Don't Block on Async Code (Stephen Cleary)

What Causes the Deadlock Here’s the situation: remember from my intro post that after you await a Task, when the method continues it will continue in a context.

In the first case, this context is a UI context (which applies to any UI except Console applications). In the second case, this context is an ASP.NET request context.

One other important point: an ASP.NET request context is not tied to a specific thread (like the UI context is), but it does only allow one thread in at a time. This interesting aspect is not officially documented anywhere AFAIK, but it is mentioned in my MSDN article about SynchronizationContext.

So this is what happens, starting with the top-level method (Button1_Click for UI / MyController.Get for ASP.NET):

  1. The top-level method calls GetJsonAsync (within the UI/ASP.NET context).
  2. GetJsonAsync starts the REST request by calling HttpClient.GetStringAsync (still within the context).
  3. GetStringAsync returns an uncompleted Task, indicating the REST request is not complete.
  4. GetJsonAsync awaits the Task returned by GetStringAsync. The context is captured and will be used to continue running the GetJsonAsync method later. GetJsonAsync returns an uncompleted Task, indicating that the GetJsonAsync method is not complete.
  5. The top-level method synchronously blocks on the Task returned by GetJsonAsync. This blocks the context thread.
  6. … Eventually, the REST request will complete. This completes the Task that was returned by GetStringAsync.
  7. The continuation for GetJsonAsync is now ready to run, and it waits for the context to be available so it can execute in the context.
  8. Deadlock. The top-level method is blocking the context thread, waiting for GetJsonAsync to complete, and GetJsonAsync is waiting for the context to be free so it can complete.

For the UI example, the “context” is the UI context; for the ASP.NET example, the “context” is the ASP.NET request context. This type of deadlock can be caused for either “context”.

Preventing the Deadlock There are two best practices (both covered in my intro post) that avoid this situation:

  • In your “library” async methods, use ConfigureAwait(false) wherever possible.
  • Don’t block on Tasks; use async all the way down.

switch this code:

new DCMReportRetriever().Run().Wait();

to async analog:

await new DCMReportRetriever().Run();

or even this:

await new DCMReportRetriever().Run().ConfigureAwait(false);

However, the .NET Framework doesn't allow the Main method to be async, so you need to add the "proxy" method to your app, something like this:

public static void Main()
{
    MyMethodAsync().Wait();
}

static async Task MyMethodAsync()
{
    await new DCMReportRetriever().Run().ConfigureAwait(continueOnCapturedContext: false);
}

private async Task Run()
{

    ...

    foreach (_file f in files)
    {
        if (f.Status == "REPORT_AVAILABLE" && f.LastModifiedTime >= startDateSinceEpoch)
        {
            using (var stream = new FileStream(f.FileName + ".csv", FileMode.Append))
            {
                new MediaDownloader(service).Download(f.Urls.ApiUrl, stream);
            }
        }
    }
}

You can find more explanations in this article:

Best Practices in Asynchronous Programming

Upvotes: 2

Related Questions