最近我读了一篇关于 Habré 的文章(更新:我从评论中意识到我需要附上一个引用,关于这个问题是进一步的)
一旦代码到达 Task.Run() 方法,就会从线程池中取出另一个线程,并在其中执行我们传递给 Task.Run() 的代码。旧线程,作为一个体面的线程,返回到池中并等待再次被调用以完成工作。新线程执行传递的代码,到达同步操作,同步执行(等待操作完成),然后继续执行代码。换句话说,操作一直保持同步:我们和以前一样,在同步操作执行期间使用线程。唯一的区别是我们在调用 Task.Run() 和返回 ExecuteOperation() 时花费了时间切换上下文。一切都变得更糟了。
那里讨论的问题之一是调用Task.Run
是一种反模式,它仅用于 GUI 响应性。
问题在于它在Task.Run(() => _anyWork())
哪里_anyWork()
包含同步代码。如果你这样做,文章中写的内容听起来很合乎逻辑:
await DoWork();
...
Task DoWork() => Task.Run(_work);
是的,在这种情况下,会在线程池上创建额外的负载。但是,如果你这样做:
var task1 = DoWork1();
var task2 = DoWork2();
var task3 = DoWork3();
await Task.WhenAll(task1, task2, task3);
Task.Run
立即变成普通代码,对吗?
将执行此代码的线程将创建三个其他线程(upd : speculative: 启动将工作添加到要运行的队列中ThreadPool
),它们将并行执行它们的工作。此外,当他遇到 时await
,他将返回控制权(最终,他很可能会返回池中)。如果不是请更正。
如果是这样,问题就来了:这条线在哪里,在一个糟糕的实现和一个正常的实现之间?在非库代码中,很明显 - 如果调用了一个方法,并且等待在更远的地方,那么您可以执行Task.Run
. 一方面,在库中,如果客户端在调用后等待它们,我们可以并行化方法的工作。另一方面,如果客户端在调用时期望立即得到结果,我们可以徒劳地增加负载。我们无法确切知道客户端将如何调用这些方法,我们只能在文档中给出建议。
也许有一些来自 MS 的官方建议?在 msdn 上,我只找到了对这些方法如何工作的干巴巴的描述。
任务运行方法
也就是说,Task.Run 是一种在线程池中执行某些工作的方法,能够异步等待结果。
什么时候可能需要它?可能的用例:
1) 启动 IO/CPU 加载池线程,以免加载主 UI 线程(示例)
2) 从 UI 线程同步运行异步代码。当您有一个大型应用程序并且您逐渐切换到异步调用而不是同步调用时,这可能是必需的,但并非在任何地方您仍然可以异步调用您的新 API,但是您需要避免 UI 线程中的死锁,例如
Task.Run(()=>CallSmthgAsync().GetAwaiter().GetResult()).GetAwaiter().GetResult();
- 这个不是很漂亮的代码,应该修好了,但并不总是可以一次性实现异步调用,所以我自己做了,有时也看到了。何时不要:当您不关心当前线程被某些同步工作阻塞的事实时。
在问题的示例中,任务已经在线程池中运行,因此切换到另一个线程并在那里执行同步操作是没有意义的。但是如果从 UI 线程启动相同的工作,那么如果没有
Task.Run
整个 UI,应用程序就会崩溃。至于图书馆代码。根据需求和用例构建您的 API 及其实现。如果您有很多 I/O 操作(使用文件、使用网络、使用数据库),那么这本身就暗示需要异步 API。如果您对选择同步 API 和异步 API 感到困惑,那么请同时实现这两种 API,让客户端选择他需要的。
因此得出结论:最重要的是,为了不破坏柴火,始终知道自己在做什么以及为什么要这样做。当你有理由这样做时切换上下文。如果你的每一行代码都是合理的,并且有理由这样写,那么你在选择好代码/坏代码方面的问题就会少得多。
*Async 方法代码在当前线程上执行,直到第一个 await。并且当前线程将被捕获。比如HttpClient在开始异步请求之前同步请求dns,这可能是一个很长的操作。
而且您可以决定更担心的是什么 - 等待当前线程或 Task.Run() 的开销(实际上这是最小的,除了臃肿的代码)
使用
Task
:using(IDisposable)
潜在错误的有趣文章Task
) - 经常使用TaskCompletionSource
第二个看起来像一个
I/O-bound
操作 - 我不确定是否将它写为一个单独的项目