为什么会出现这种异步操作挂起?(Why does this async action hang?)

2019-07-18 17:20发布

我有一个多层次的.NET 4.5的应用程序调用使用C#的新方法asyncawait关键字,只是挂,我不明白为什么。

在底部我有程度上我们的数据库应用程序的异步方法OurDBConn (基本上为基础的包装DBConnectionDBCommand对象):

public static async Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    T result = await Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });

    return result;
}

然后,我有调用此得到一些慢速运行总计中等水平异步方法:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var result = await this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));

    return result;
}

最后,我有一个UI法(MVC动作)运行同步:

Task<ResultClass> asyncTask = midLevelClass.GetTotalAsync(...);

// do other stuff that takes a few seconds

ResultClass slowTotal = asyncTask.Result;

问题是,它挂在最后一行永远。 它做同样的事情,如果我叫asyncTask.Wait() 如果我直接运行缓慢SQL方法大约需要4秒。

我期待的行为是当它到达asyncTask.Result ,如果这还不算完,应该等到它,一旦它应该返回结果。

如果我有一个调试器步骤通过SQL语句完成和lambda函数完成,但return result; 线GetTotalAsync则永远无法实现。

任何想法,我做错了什么?

在那里我需要为了调查任何建议,以解决这一问题?

难道这是一个僵局的地方,如果是的话有没有什么直接的方式找到它?

Answer 1:

是的,这是一个僵局没事。 并与TPL一个常见的错误,所以不心疼。

当你写await foo ,运行时,默认情况下,时间表上,该方法开始于同一的SynchronizationContext功能的延续。 在英语中,我们说你叫你ExecuteAsync从UI线程。 您所查询的线程池线程上运行(因为你叫Task.Run ),但你等待的结果。 这意味着,运行时会安排你的“ return result; ”行跑回来在UI线程上,而不是安排回线程池。

那么,如何做到这一点的僵局? 想象一下,你刚刚有这样的代码:

var task = dataSource.ExecuteAsync(_ => 42);
var result = task.Result;

所以,第一行揭开序幕异步工作。 然后,第二行块UI线程 。 因此,当运行时要运行的“返回结果”行后面的UI线程上,它不能做到这一点,直到Result完成。 但当然,不能给出结果,直到返回发生。 僵局。

这说明使用TPL的一个重要法则:当您使用.Result在UI线程(或其它精美的同步上下文中),你一定要小心,以确保没有什么任务取决于定于UI线程。 否则邪恶发生。

所以你会怎么做? 选项1是使用等待无处不在,但正如你说这已经不是一种选择。 第二个选项,可供您是简单地停止使用的await。 你可以重写你的两个函数:

public static Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    return Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });
}

public static Task<ResultClass> GetTotalAsync( ... )
{
    return this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));
}

有什么不同? 现在有没有在任何地方等待,所以没有什么隐含地调度到UI线程。 对于简单的方法,如这些具有单一的回报,有在做一个“不点var result = await...; return result ”的格局; 只是删除异步改性剂和直接传递任务对象周围。 它的开销少,如果没有别的。

选项#3是指定你不想让你的等待着安排回UI线程,而只是安排到UI线程。 你这样做与ConfigureAwait方法,就像这样:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var resultTask = this.DBConnection.ExecuteAsync<ResultClass>(
        ds => return ds.Execute("select slow running data into result");

    return await resultTask.ConfigureAwait(false);
}

通常等待一个任务会如果你是它安排到UI线程; 等待的结果ContinueAwait会忽略你在任何情况下,始终对调度线程池。 这种方法的缺点是你必须在你的。结果取决于所有功能处处洒这一点,因为错过任何.ConfigureAwait可能是另一个僵局的原因。



Answer 2:

这是典型的混色async死锁情况, 因为我描述我的博客上 。 杰森描述得好:默认情况下,“上下文”被保存在每一个await和用于继续async方法。 这种“上下文”是当前SynchronizationContext除非它null ,在这种情况下,它是当前TaskScheduler 。 当async方法试图继续时,它首先重新进入捕获“上下文”(在这种情况下,ASP.NET SynchronizationContext )。 在ASP.NET SynchronizationContext只允许在上下文一个线程在同一时间,且已经存在的背景下,一个线程-线程阻塞Task.Result

有两个准则,以避免这种僵局:

  1. 使用async一路下跌。 你提到你“不能”这样做,但我不知道为什么。 ASP.NET MVC的.NET 4.5肯定可以支持async操作,这是不是一个困难的变化作出。
  2. 使用ConfigureAwait(continueOnCapturedContext: false)尽可能。 这将覆盖恢复对捕捉的上下文的默认行为。


Answer 3:

我是在同一个死锁的情况,但在我的情况下调用从一个同步方法异步方法,对我有什么工作是:

private static SiteMetadataCacheItem GetCachedItem()
{
      TenantService TS = new TenantService(); // my service datacontext
      var CachedItem = Task.Run(async ()=> 
               await TS.GetTenantDataAsync(TenantIdValue)
      ).Result; // dont deadlock anymore
}

这是一个很好的办法,任何想法?



Answer 4:

只需添加到接受的答案(没有足够的代表评论),我不得不使用时出现堵塞这个问题task.Result ,事件虽然每次await它下面有ConfigureAwait(false) ,如下例所示:

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{ 
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

这个问题实际上奠定与外部库代码。 异步库方法试图继续在调用sync背景下,无论我怎么配置的await,导致死锁。

因此,答案是推出自己的外部库代码的版本ExternalLibraryStringAsync ,以便它具有所需的延拓性。


错误答案出于历史目的

很多创伤和痛苦之后,我找到了解决埋在这个博客帖子 (按Ctrl-F为“僵局”)。 它围绕着使用task.ContinueWith ,而不是裸露task.Result

此前死锁例如:

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{ 
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

避免这样的僵局:

public Foo GetFooSynchronous
{
    var foo = new Foo();
    GetInfoAsync()  // ContinueWith doesn't run until the task is complete
        .ContinueWith(task => foo.Info = task.Result);
    return foo;
}

private async Task<string> GetInfoAsync
{
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}


文章来源: Why does this async action hang?