TPL 和 async/await (线程处理)之间的区别



试图理解TPL和async/await在线程创建方面的区别。

我相信 TPL(TaskFactory.StartNew )的工作方式与ThreadPool.QueueUserWorkItem类似,因为它在线程池中的线程上排队工作。当然,除非您使用创建新线程的TaskCreationOptions.LongRunning

我认为async/await本质上会以类似的方式工作:

TPL:

Factory.StartNew( () => DoSomeAsyncWork() )
.ContinueWith( 
    (antecedent) => {
        DoSomeWorkAfter(); 
    },TaskScheduler.FromCurrentSynchronizationContext());

Async/Await

await DoSomeAsyncWork();  
DoSomeWorkAfter();

将是相同的。从我一直在阅读的内容来看,似乎async/await只有"有时"会创建一个新线程。那么它什么时候创建一个新线程,什么时候不创建一个新线程呢?如果您正在处理 IO 完成端口,我可以看到它不必创建新线程,否则我认为它必须这样做。我想我对FromCurrentSynchronizationContext的理解也总是有点模糊。从本质上讲,我总是通过它来表达 UI 线程。

我相信TPL(TaskFactory.Startnew)的工作方式类似于ThreadPool.QueueUserWorkItem,因为它将线程池中线程上的工作排队。

几乎。

从我一直在阅读的内容来看,似乎 async/await 只是"有时"创建一个新线程。

实际上,它永远不会发生。如果你想要多线程,你必须自己实现它。有一种新的Task.Run方法只是Task.Factory.StartNew的简写,它可能是在线程池上启动任务的最常见方法。

如果您正在处理 IO 完成端口,我可以看到它不必创建新线程,否则我认为它必须这样做。

宾果游戏。因此,像Stream.ReadAsync这样的方法实际上会围绕 IOCP 创建一个Task包装器(如果Stream具有 IOCP)。

您还可以创建一些非 I/O、非 CPU"任务"。一个简单的例子是 Task.Delay ,它返回一个在一段时间后完成的任务。

关于 async/await 的很酷的事情是,您可以将一些工作排队到线程池(例如,Task.Run),执行一些 I/O 绑定操作(例如,Stream.ReadAsync ),并执行一些其他操作(例如,Task.Delay)......它们都是任务!它们可以等待或以组合使用,例如 Task.WhenAll .

任何返回Task的方法都可以await - 它不必是async方法。因此,Task.Delay和 I/O 绑定操作只是使用 TaskCompletionSource 来创建和完成任务 - 线程池上唯一完成的是事件发生时的实际任务完成(超时、I/O 完成等)。

我想我对FromCurrentSynchronizationContext的理解也总是有点模糊。从本质上讲,我总是通过它来表达 UI 线程。

我写了一篇关于SynchronizationContext的文章.大多数时候,SynchronizationContext.Current

  • 如果当前线程是 UI 线程,则为 UI 上下文。
  • 是 ASP.NET 请求上下文(如果当前线程正在为 ASP.NET 请求提供服务)。
  • 否则是线程池上下文。

任何线程都可以设置自己的SynchronizationContext,因此上述规则也有例外。

请注意,如果 async 方法不为 null,则默认的 Task 等待者将在当前SynchronizationContext上调度该方法的其余部分;否则,它将在当前TaskScheduler上。这在今天并不那么重要,但在不久的将来,这将是一个重要的区别。

我在博客上写了自己的async/await介绍,Stephen Toub最近发布了一个很好的async/await常见问题解答。

关于"并发"与"多线程",请参阅此相关的 SO 问题。我会说async启用并发性,这可能是也可能不是多线程的。使用 await Task.WhenAllawait Task.WhenAny 进行并发处理很容易,除非您显式使用线程池(例如,Task.RunConfigureAwait(false)),否则您可以同时进行多个并发操作(例如,多个 I/O 或其他类型的Delay) - 并且它们不需要线程。对于这种情况,我使用术语"单线程并发",尽管在 ASP.NET 主机中,您实际上可能会得到"线程并发"。这很甜。

async/await 基本上简化了ContinueWith方法(延续传递风格)

它不会引入并发性 - 您仍然必须自己执行此操作(或使用框架方法的异步版本。

因此,C# 5 版本将是:

await Task.Run( () => DoSomeAsyncWork() );
DoSomeWorkAfter();

所以,现在是 2023 年,似乎 async/await 仍然没有得到普遍的充分理解,因为我最近刚刚努力培训了一群人......我看到这个SO Q&A,觉得我需要指出一百万年前的更正。

@Nick Butler - 很抱歉,因为我看到你是一个受人尊敬的SO成员,但需要对你11岁的评论进行更正,这样那些不太专家的人就不会感到困惑;)

尼克写道:

await Task.Run( () => DoSomeAsyncWork() );
DoSomeWorkAfter();

这实际上应该是:

await Task.Run(async() => await DoSomeAsyncWorkAsync() ); // optional: .ConfigureAwait(false)
DoSomeWorkAfter();

几点注意事项:

  1. 在最初的答案中,DoSomeAsyncWork() 实际上返回一个"可等待的"(一个任务,用于意图/目的)并不明显;显然,你的 IDE 和编译器会很快告诉你该方法实际上返回了什么......但是人类程序员不能只看就知道,这就是为什么......
  2. Microsoft创建/建议将"Async"的后缀附加到方法的"自然"名称中。注意:这不是必需的,强烈建议人类开发人员更清楚地了解 API 的意图(无需
  3. 之前提供的 lambda 本质上是同步的,即使假定从嵌套方法返回的任务是由 lambda 返回的。 lambda 可能会令人困惑 b/c 您需要取消 lambda 为您节省的工作(以运行时效率低下为代价)并使您的人类手动验证/理解 DoSomeAsyncWork() 的返回类型 - 这说明了 MS 对注释 #2 的基本原理。MS 还建议始终"观察"("等待"、保留引用或"丢弃")任何方法返回的任务(有一个静态分析器 NuGet 包来查找可以帮助解决所有这些问题的异步等待反模式)lamba 的"异步"修饰符允许异步(任务返回)方法的"等待"。
  4. ConfigureAwait(false)...https://devblogs.microsoft.com/dotnet/configureawait-faq/

我希望这对至少一个人有帮助。 我知道巴特勒很久以前就写@Nick他的答案,远在我完全理解这些东西之前。 我只是想为那些在 2023+ 中寻求它的人提供更好的指导/清晰度。

最新更新