Why does this async action hang when I try and access the Result property of my Task?

我有一个多层的。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 函数完成,但是从未到达 GetTotalAsyncreturn result;行。

知道我哪里做错了吗?

有什么建议我需要调查哪些地方来解决这个问题吗?

这会不会是某个地方的僵局,如果是这样,有没有直接的办法找到它?

66498 次浏览

是的,这确实是一个僵局。这也是 TPL 的一个常见错误,所以不要感到难过。

在编写 await foo时,默认情况下,运行库将在启动该方法的同一 SynchronizationContext 上安排函数的继续。在英语中,假设您从 UI 线程调用了 ExecuteAsync。查询在线程池线程上运行(因为您调用了 Task.Run) ,但随后等待结果。这意味着运行时将安排您的“ return result;”行在 UI 线程上运行,而不是将它安排回线程池。

那么这个死锁是如何产生的呢? 假设你有这样的代码:

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

因此,第一行开始异步工作。第二行然后是 阻塞 UI 线程。因此,当运行时希望在 UI 线程上运行“ return result”行时,在 Result完成之前不能这样做。当然,在返回发生之前,不能给出结果。僵局。

这说明了使用 TPL 的一个关键规则: 当您在 UI 线程上使用 .Result(或其他花哨的同步上下文)时,必须小心确保 Task 所依赖的任何东西都不会被安排到 UI 线程上。否则邪恶就会发生。

那你是做什么的?选项 # 1是使用等待无处不在,但正如您所说,这已经不是一个选项。第二种选择是停止使用 wait。您可以将两个函数重写为:

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 线程,而只是计划到线程池。您可以使用 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的结果将会忽略您所在的任何上下文,并且总是调度到线程池。这样做的缺点是你必须在你的所有函数中加入这个 everywhere。结果取决于此,因为任何错过的 .ConfigureAwait都可能导致另一个死锁。

这是典型的混合 async死锁场景 await0。Jason 很好地描述了它: 默认情况下,每个 await都保存一个“上下文”,并用于继续使用 async方法。这个“上下文”是当前的 SynchronizationContext,除非它是 null,在这种情况下,它是当前的 TaskScheduler。当 async方法试图继续时,它首先重新进入捕获的“上下文”(在本例中是 ASP.NET SynchronizationContext)。NET SynchronizationContext一次只允许上下文中的一个线程,而且上下文中已经有一个线程——在 Task.Result上被阻塞的线程。

有两条准则可以避免这种僵局:

  1. 一直使用 async。你说你“不能”这么做,但我不知道为什么。开启 ASP.NET MVC。NET 4.5当然可以支持 async操作,而且做出这样的改变并不困难。
  2. 尽可能多地使用 ConfigureAwait(continueOnCapturedContext: false)。这将覆盖在捕获的上下文上恢复的默认行为。

为了补充已经接受的答案(没有足够的代表来评论) ,我在使用 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);
}

问题实际上在于外部库代码。无论我如何配置等待,异步库方法都试图在调用同步上下文中继续运行,从而导致死锁。

因此,答案是滚动我自己版本的外部库代码 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);
}

我也处于同样的死锁状态,但在我的案例中,从 sync 方法调用一个异步方法,对我有效的方法是:

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

这是个好办法吗?

快速回答: 换这条线

ResultClass slowTotal = asyncTask.Result;

ResultClass slowTotal = await asyncTask;

为什么?你不应该使用。除了控制台应用程序之外,大多数应用程序都可以使用 result 来获取任务的结果,如果您这样做的话,程序到达那里时将会挂起

如果要使用.Result,也可以尝试下面的代码

ResultClass slowTotal = Task.Run(async ()=>await asyncTask).Result;