我有一个多层次的.NET 4.5的应用程序调用使用C#的新方法async
和await
关键字,只是挂,我不明白为什么。
在底部我有程度上我们的数据库应用程序的异步方法OurDBConn
(基本上为基础的包装DBConnection
和DBCommand
对象):
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
则永远无法实现。
任何想法,我做错了什么?
在那里我需要为了调查任何建议,以解决这一问题?
难道这是一个僵局的地方,如果是的话有没有什么直接的方式找到它?
是的,这是一个僵局没事。 并与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
可能是另一个僵局的原因。
这是典型的混色async
死锁情况, 因为我描述我的博客上 。 杰森描述得好:默认情况下,“上下文”被保存在每一个await
和用于继续async
方法。 这种“上下文”是当前SynchronizationContext
除非它null
,在这种情况下,它是当前TaskScheduler
。 当async
方法试图继续时,它首先重新进入捕获“上下文”(在这种情况下,ASP.NET SynchronizationContext
)。 在ASP.NET SynchronizationContext
只允许在上下文一个线程在同一时间,且已经存在的背景下,一个线程-线程阻塞Task.Result
。
有两个准则,以避免这种僵局:
- 使用
async
一路下跌。 你提到你“不能”这样做,但我不知道为什么。 ASP.NET MVC的.NET 4.5肯定可以支持async
操作,这是不是一个困难的变化作出。 - 使用
ConfigureAwait(continueOnCapturedContext: false)
尽可能。 这将覆盖恢复对捕捉的上下文的默认行为。
我是在同一个死锁的情况,但在我的情况下调用从一个同步方法异步方法,对我有什么工作是:
private static SiteMetadataCacheItem GetCachedItem()
{
TenantService TS = new TenantService(); // my service datacontext
var CachedItem = Task.Run(async ()=>
await TS.GetTenantDataAsync(TenantIdValue)
).Result; // dont deadlock anymore
}
这是一个很好的办法,任何想法?
只需添加到接受的答案(没有足够的代表评论),我不得不使用时出现堵塞这个问题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);
}