是的,这是一个僵局。这是TPL的常见错误,所以不要感到难过。
当您编写
awaitfoo时,默认情况下,运行时将在该方法开始的同一SynchronizationContext上调度函数的继续。用英语来说,假设您是
ExecuteAsync从UI线程调用的。您的查询在线程池线程上运行(因为您调用了
Task.Run),但是随后等待结果。这意味着运行时将安排您的“
return result;”行在UI线程上运行,而不是将其安排回线程池。
那么,这种僵局又如何呢?假设您只有以下代码:
var task = dataSource.ExecuteAsync(_ => 42);var result = task.Result;
因此,第一行开始了异步工作。然后第二行 阻塞UI线程
。因此,当运行时想要在UI线程上重新运行“返回结果”行时,它必须等到
Result完成后才能执行。但是,当然,要等到返回发生后才能给出结果。僵局。
这说明了使用TPL的关键规则:在
.ResultUI线程(或其他一些花哨的同步上下文)上使用时,必须小心确保不会将Task所依赖的任何项目安排到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线程,而只是调度到线程池。您可以使用
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将忽略您所处的任何上下文,并始终计划到线程池中。这样做的缺点是您必须在.Result所依赖的所有函数中的
任何地方 都撒上它,因为任何遗漏都
.ConfigureAwait可能导致另一个死锁。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)