代码在system.Threading.Tasks.Task方法中没有“完成”

时间:2015-01-12 09:43:25

标签: c# .net asynchronous async-await

我从Main调用了一个带有.Wait()的system.Threading.Tasks.Task方法。该方法最后有一个return语句,我希望表示该任务已完成",允许Main继续执行。然而,返回语句被命中(使用断点调试)但是执行不会在Main中继续,而是似乎只是"挂起",没有进一步执行。

代码在这里。

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

编辑:我应该补充一点,Main是我的入口点,显然是静态的async void主要不好?

1 个答案:

答案 0 :(得分:1)

这是异常/等待状态机通过上下文切换导致死锁的常见错误。

你可以在这里找到一个清楚的解释:

Don't Block on Async Code(Stephen Cleary)

  
    

导致死锁的原因     情况就是这样:请记住我的介绍帖,等待任务后,当方法继续时,它将继续在上下文中。

         

在第一种情况下,此上下文是UI上下文(适用于除控制台应用程序之外的任何UI)。在第二种情况下,此上下文是ASP.NET请求上下文。

         

另一个重点:ASP.NET请求上下文不依赖于特定线程(如UI上下文),但它一次只允许一个线程。这个有趣的方面在AFAIK的任何地方都没有正式记录,但我的MSDN文章中提到了有关SynchronizationContext的内容。

         

所以这就是发生的事情,从顶级方法开始(用于ASP.NET的UI / MyController.Get的Button1_Click):

         
        
  1. 顶级方法调用GetJsonAsync(在UI / ASP.NET上下文中)。
  2.     
  3. GetJsonAsync通过调用HttpClient.GetStringAsync(仍然在上下文中)启动REST请求。
  4.     
  5. GetStringAsync返回未完成的任务,表示REST请求未完成。
  6.     
  7. GetJsonAsync等待GetStringAsync返回的任务。捕获上下文,稍后将用于继续运行GetJsonAsync方法。 GetJsonAsync返回未完成的Task,表示GetJsonAsync方法未完成。
  8.     
  9. 顶级方法同步阻止GetJsonAsync返回的任务。这会阻止上下文线程。
  10.     
  11. ...最终,REST请求将完成。这样就完成了GetStringAsync返回的任务。
  12.     
  13. GetJsonAsync的延续现在已准备好运行,它等待上下文可用,以便它可以在上下文中执行。
  14.     
  15. 死锁。顶级方法是阻塞上下文线程,等待GetJsonAsync完成,GetJsonAsync正在等待上下文空闲,以便它可以完成。
  16.               

    对于UI示例,“context”是UI上下文;对于ASP.NET示例,“context”是ASP.NET请求上下文。这种类型的死锁可能会导致“上下文”。

             

    防止死锁     避免这种情况有两种最佳实践(我的介绍中都包含这些内容):

             
          
    • 在“库”异步方法中,尽可能使用ConfigureAwait(false)。
    •     
    • 不要阻止任务;一直使用async。
    •     
      

切换此代码:

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

async analog:

await new DCMReportRetriever().Run();

甚至是这样:

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

但是,.NET Framework不允许Main方法异步,因此您需要在应用程序中添加“代理”方法,如下所示:

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);
            }
        }
    }
}

您可以在本文中找到更多解释:

Best Practices in Asynchronous Programming