许多等待Async方法,或者单个等待包装任务
本文关键字:等待 单个 包装 任务 或者 Async 方法 许多 | 更新日期: 2023-09-27 18:17:14
假设我们必须通过异步流在数据库上写下一个包含1000个元素的列表。是等待异步插入语句1000次更好,还是将所有1000次插入都封装在一个同步方法中,封装到一个Task.Run
语句中,等待一次更好?
例如,SqlCommand
有每个方法与他的async
版本耦合。在这种情况下,我们有一个插入语句,所以我们可以调用ExecuteNonQuery
或ExecuteNonQueryAsync
。
通常,在async/await指南中,我们读到如果你有一个异步版本的方法,你应该使用它。假设我们写:
async Task Save(IEnumerable<Savable> savables)
{
foreach(var savable in savables)
{
//create SqlCommand somehow
var sqlCmd = CreateSqlCommand(savable);
//use asynchronous version
await sqlCmd.ExecuteNonQueryAsync();
}
}
这段代码非常清晰。然而,每次它从等待部分出去时,它也返回UI线程,然后在下一次遇到等待时返回后台线程,等等(不是吗?)这意味着用户可以看到一些延迟,因为UI线程不断被await
的延续中断,以执行下一个foreach
周期,并且在这段时间内UI冻结了一点。
我想知道我是否应该这样写代码:
async Task Save(IEnumerable<Savable> savables)
{
await Task.Run(() =>
{
foreach(var savable in savables)
{
//create SqlCommand somehow
var sqlCmd = CreateSqlCommand(savable);
//use synchronous version
sqlCmd.ExecuteNonQuery();
}
});
}
这样,整个foreach
在二级线程上执行,而不需要在UI线程和二级线程之间不断切换。这意味着UI线程可以在foreach
的整个持续时间内自由地更新View(例如一个旋转器或进度条),也就是说,用户不会感觉到延迟。
我说的对吗?还是我错过了什么"异步"?
我不是在寻找简单的基于意见的答案,我在寻找async/await指南的解释,在这种情况下,以及解决它的最佳方法。
编辑:我读过这个问题,但它是不一样的。这个问题是关于在异步方法上选择单个await
还是单个Task.Run
await。这个问题是关于调用1000 await
的后果和由于线程之间的持续切换而导致的资源开销。
你的分析基本上是正确的。你似乎高估了这会给UI线程带来的负担;它被要求做的实际工作是相当小的,所以它能够保持良好的可能性,但也有可能你做的足够多,它不能,所以你有兴趣在UI线程上不执行延续是正确的。
你错过的当然是避免所有UI线程回调的首选方法。当您await
一个操作时,如果您实际上不需要方法的其余部分返回到原始上下文,您可以简单地将ConfigureAwait(false)
添加到您正在等待的任务的末尾。这将阻止continuation在当前上下文(即UI线程)中运行,而让continuation在线程池线程中运行。
使用ConfigureAwait(false)
允许您避免UI负责不必要的非UI工作,同时也防止您需要调度线程池线程做更多的工作。
当然,如果你的工作结束后做的工作实际上是要做UI工作,那么该方法不应该使用ConfigureAwait(false);
,因为它实际上希望在UI线程上调度continuation。
这一切都取决于UI所期望的。如果UI依赖于操作的完成,以保持一个有效的状态,那么你别无选择,只能等待异步任务,或者像往常一样在主UI线程上使用调用。
对于需要很长时间但不是调用线程立即需要的任务,或者调用线程依赖于操作的结果的任务,线程化是很好的。
任务的存在不是为了加快速度,而是为了提高效率。因此,你可以保留任务线程并引发一个带有"操作完成"的事件,让UI在操作发生时正常运行,但就像我说的,如果UI线程依赖于结果,你别无选择,只能等待。如果你通过事件路由,你可以异步更新你的UI与结果,因为他们进来
这两个解决方案之间有一个重要的区别。第一个是单线程的(除非你使用ConfigureAwait(false)
@Servy建议),而第二个是多线程的。
多线程总是给你的程序带来额外的复杂性。你应该先问问自己,你是否愿意用这些来换取你可能获得的好处。