许多等待异步方法,还是单个等待包装 Task.Run?
Many awaits for Async method, or a single await for a wrapping Task.Run?
假设我们必须通过异步流在数据库中记录一个包含 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();
}
}
这段代码很清楚。然而,每次它从 await 部分出去时,它也在 UI 线程上 returns,然后在下一个遇到的 await 中回到后台线程,依此类推(不是吗? ).这意味着用户可以看到一些滞后,因为 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
的整个持续时间内自由更新视图(例如微调器或进度条),也就是说,用户不会感觉到延迟。
我说得对吗?还是我遗漏了一些关于 "async all the way down" 的信息?
我不是在寻找简单的基于意见的答案,我是在寻找 async/await 准则的解释,以防出现这种情况,并找到解决问题的最佳方法。
编辑:
我读过 但不一样。这个问题是关于在异步方法上选择单个 await
还是选择单个 Task.Run
等待。这个问题是关于调用 1000 await
的后果以及由于线程之间不断切换而导致的资源开销。
这完全取决于 UI 的期望。如果 UI 依赖于操作完成以保持有效状态,那么您别无选择,只能等待异步任务或像往常一样在主 UI 线程上调用。
线程非常适合需要很长时间但调用线程不需要立即执行或调用线程依赖于操作结果的任务。
任务不是为了加快速度,而是为了提高效率。因此,您可以保留任务线程并引发一个带有 'operation completed' 的事件,让 UI 在此操作发生时正常运行,但就像我说的 UI 线程是否取决于结果,你别无选择,只能等待。
如果您通过事件路线,您可以在结果出现时异步更新您的UI
您的分析基本正确。您似乎高估了这会给 UI 线程带来的负担;它被要求做的实际工作相当小,所以它能够保持良好状态的可能性很大,但有可能你做的已经足够多了,所以它做不到,所以你是对的有兴趣不在 UI 线程上执行延续。
当然,您缺少的是避免所有回调到 UI 线程的首选方法。当您 await
一个操作时,如果您实际上不需要方法的其余部分 return 返回到原始上下文,您可以简单地将 ConfigureAwait(false)
添加到您执行的任务的末尾正在等待。这将防止在当前上下文(即 UI 线程)中继续 运行ning,而是让继续 运行 在线程池线程中。
使用 ConfigureAwait(false)
可以避免 UI 负责非 UI 不必要的工作,同时还可以防止您需要安排线程池线程做比它们更多的工作需要做的。
当然,如果您在继续后最终完成的工作实际上是 UI 工作,那么该方法不应该使用 ConfigureAwait(false);
,因为它实际上 想要在UI线程上安排继续。
这两种解决方案之间有一个重要区别。第一个是单线程的(除非您按照@Servy 的建议使用 ConfigureAwait(false)
),而第二个是多线程的。
多线程总是会给您的程序带来额外的复杂性。你应该首先问问自己,你是否愿意用它来换取你可能获得的利益。
假设我们必须通过异步流在数据库中记录一个包含 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();
}
}
这段代码很清楚。然而,每次它从 await 部分出去时,它也在 UI 线程上 returns,然后在下一个遇到的 await 中回到后台线程,依此类推(不是吗? ).这意味着用户可以看到一些滞后,因为 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
的整个持续时间内自由更新视图(例如微调器或进度条),也就是说,用户不会感觉到延迟。
我说得对吗?还是我遗漏了一些关于 "async all the way down" 的信息?
我不是在寻找简单的基于意见的答案,我是在寻找 async/await 准则的解释,以防出现这种情况,并找到解决问题的最佳方法。
编辑:
我读过 await
还是选择单个 Task.Run
等待。这个问题是关于调用 1000 await
的后果以及由于线程之间不断切换而导致的资源开销。
这完全取决于 UI 的期望。如果 UI 依赖于操作完成以保持有效状态,那么您别无选择,只能等待异步任务或像往常一样在主 UI 线程上调用。
线程非常适合需要很长时间但调用线程不需要立即执行或调用线程依赖于操作结果的任务。
任务不是为了加快速度,而是为了提高效率。因此,您可以保留任务线程并引发一个带有 'operation completed' 的事件,让 UI 在此操作发生时正常运行,但就像我说的 UI 线程是否取决于结果,你别无选择,只能等待。
如果您通过事件路线,您可以在结果出现时异步更新您的UI
您的分析基本正确。您似乎高估了这会给 UI 线程带来的负担;它被要求做的实际工作相当小,所以它能够保持良好状态的可能性很大,但有可能你做的已经足够多了,所以它做不到,所以你是对的有兴趣不在 UI 线程上执行延续。
当然,您缺少的是避免所有回调到 UI 线程的首选方法。当您 await
一个操作时,如果您实际上不需要方法的其余部分 return 返回到原始上下文,您可以简单地将 ConfigureAwait(false)
添加到您执行的任务的末尾正在等待。这将防止在当前上下文(即 UI 线程)中继续 运行ning,而是让继续 运行 在线程池线程中。
使用 ConfigureAwait(false)
可以避免 UI 负责非 UI 不必要的工作,同时还可以防止您需要安排线程池线程做比它们更多的工作需要做的。
当然,如果您在继续后最终完成的工作实际上是 UI 工作,那么该方法不应该使用 ConfigureAwait(false);
,因为它实际上 想要在UI线程上安排继续。
这两种解决方案之间有一个重要区别。第一个是单线程的(除非您按照@Servy 的建议使用 ConfigureAwait(false)
),而第二个是多线程的。
多线程总是会给您的程序带来额外的复杂性。你应该首先问问自己,你是否愿意用它来换取你可能获得的利益。