假设我们必须通过异步流向数据库写下1000个元素的列表。是等待1000次异步插入语句,还是将所有1000个插入包装在一个封装在Task.Run
语句中的单个同步方法中,等待一次?
例如,SqlCommand
的每种方法都与他的async
版本相结合。在这种情况下,我们有一个insert语句,因此我们可以调用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();
}
}
这段代码很清楚。但是,每次从await部分出来时,它也会在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(例如微调器或进度条),也就是说,用户不会感觉到滞后。
我是对的吗?或者我错过了关于&#34; async的所有方式&#34;?
我没有寻找简单的基于意见的答案,我正在寻找异步/等待指南的解释,以及最佳解决方法。
修改
我已阅读this question,但不一样。这个问题是关于在异步方法上选择单个await
而不是单个Task.Run
等待。这个问题是关于拨打1000 await
和资源&#39;的后果。由于线程之间的连续切换而产生的开销。
答案 0 :(得分:5)
您的分析基本上是正确的。你似乎高估了这会给UI线程带来的负担;要求它做的实际工作量相当小,所以很可能它能够保持良好状态,但是你可能做得足够多,以至于它无法做到这一点。因此,您有兴趣不在UI线程上执行延续。
您当然缺少的是避免对UI线程进行所有回调的首选方法。当您await
进行操作时,如果您实际上不需要该方法的其余部分返回原始上下文,则只需将ConfigureAwait(false)
添加到您执行的任务的末尾即可。等待。这将阻止继续在当前上下文(即UI线程)中运行,而是让继续在线程池线程中运行。
使用ConfigureAwait(false)
可以避免UI对不必要的非UI工作负责,同时还可以防止您需要调度线程池线程来完成比他们需要做的更多的工作。
当然,如果你在继续之后最终做的工作实际上是要做UI工作,那么该方法不应该使用ConfigureAwait(false);
,因为它实际上想要在UI线程上安排继续。
答案 1 :(得分:0)
这完全取决于用户界面的期望。如果UI依赖于正在完成的操作以保持有效状态,那么您别无选择,只能等待异步任务或在主UI线程上使用通常的调用。
线程非常适合需要很长时间但是调用线程不需要立即执行的任务,或者调用线程依赖于操作的结果。
任务无法加快速度,它们可以提高效率。因此,您可以保留任务线程并引发一个“操作已完成”的事件,让UI在此操作发生时正常运行,但就像我说如果UI线程依赖于结果,您别无选择,只能等待。
如果你通过事件路线,你可以用结果异步更新你的用户界面
答案 2 :(得分:-2)
两种解决方案之间存在一个重要区别。第一个是单线程(除非你使用@Servy建议的ConfigureAwait(false)
),而第二个是多线程的。
多线程总是会给程序带来额外的复杂性。你应该首先问自己,你是否愿意为了获得的利益进行交易。