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

76

试图理解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线程。


4
实际上,TaskCreationOptions.LongRunning 并不能保证创建一个“新线程”。根据 MSDN 的说明,“LongRunning” 选项只是向任务调度器提供了一种提示,而并不保证会创建一个专门的线程。我通过困难的方式得知这一点。 - eduncan911
@eduncan911 虽然你所说的文档内容是正确的,但我曾经查看过TPL源代码,我非常确定当指定了 TaskCreationOptions.LongRunning 时,实际上总是会创建一个新的专用线程。 - Zaid Masud
@ZaidMasud:你可能需要再看一下。我知道它正在汇集线程,因为对于几百毫秒的短暂运行线程,Thread.CurrentThread.IsThreadPoolThread返回true。更不用说我使用的ThreadStatic变量渗透到多个线程中,导致各种混乱。我不得不强制我的代码新建多个Thread(),以保证专用线程。换句话说,我不能使用TaskFactory来获得专用线程。或者,您可以实现自己的TaskScheduler,始终返回专用线程。 - eduncan911
3个回答

90
我认为 TPL(TaskFactory.Startnew)与 ThreadPool.QueueUserWorkItem 类似,都是在线程池中排队等待工作。

基本上是这样

根据我所了解的,异步/等待似乎只有“有时”才会创建新线程。

实际上,它从未创建过。如果您想要多线程,则必须自己实现。有一个新的 Task.Run 方法只是 Task.Factory.StartNew 的简写,这可能是在线程池上启动任务的最常见方法。

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

Bingo。因此,像 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 的文章 an article。大部分时间,SynchronizationContext.Current
  • 如果当前线程是 UI 线程,则为 UI 上下文。
  • 如果当前线程正在服务一个 ASP.NET 请求,则为 ASP.NET 请求上下文。
  • 否则为线程池上下文。

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

请注意,默认的 Task 等待程序会在当前 SynchronizationContext 不为空时将剩余的 async 方法安排在其中;否则它会在当前的 TaskScheduler 中进行。这对今天来说并不那么重要,但在不久的将来,这将是一个重要的区别。

我在我的博客上写了一篇关于async/await介绍的文章,而Stephen Toub最近发布了一个优秀的async/await常见问题解答

关于“并发”和“多线程”,请参阅这个相关的SO问题。我会说async实现了并发,但不一定是多线程的。使用await Task.WhenAllawait Task.WhenAny很容易实现并发处理,除非您明确地使用线程池(例如Task.RunConfigureAwait(false)),否则可以同时进行多个并发操作(例如多个I/O或其他类型,如Delay)-而且它们不需要线程。我将这种情况称为“单线程并发”,尽管在ASP.NET主机中,您实际上可以获得“零线程并发”。这相当棒。

2
不错的回答。我还推荐http://www.infoq.com/articles/Async-API-Design和这个优秀的演示文稿:http://channel9.msdn.com/Events/TechEd/Europe/2013/DEV-B318。 - Philippe
第一个链接已失效。 - Felipe Deveza
"async/await FAQ"的链接已失效。 - Artemious
是的,微软把很多东西都搬了一遍,导致几乎所有链接都失效了。这个答案接受PR。 - Stephen Cleary

9

async/await基本上简化了ContinueWith方法(以Continuation Passing Style的形式)

它并不引入并发 - 你仍然需要自己完成这个操作(或使用框架方法的Async版本)。

因此,C#5版本为:

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

在我上面的示例中,DoSomeAsyncWork(async/wait版本)在哪里运行?如果它在UI线程上运行,那么它如何不会阻塞呢? - coding4fun
1
如果DoSomeWorkAsync()返回void或者不可等待的对象,那么你的await示例将无法编译。从你的第一个示例中,我认为这是一个你想在不同线程上运行的顺序方法。如果你将其更改为返回一个Task,而不引入并发,则会阻塞。从UI线程上看,它将按顺序执行,就像普通代码一样。只有当方法返回尚未完成的可等待对象时,await才会产生作用。 - Nick Butler
好的,我不会说它随意运行。您已经使用Task.Run来执行DoSomeAsyncWork中的代码,因此在这种情况下,您的工作将在线程池线程上完成。 - Michael Ray Lovett
我喜欢你回答的简洁明了。 - RBT

2

所以,现在是2023年,看起来async/await仍然不是被普遍理解的,因为我最近刚刚经历了对一群人进行培训的过程……我看到了这个SO问答,并感觉需要纠正一个来自一百万年前的评论。

@Nick Butler——非常抱歉,我看到你是一个备受尊敬的SO成员,但需要更正你11年前的评论,以便那些不太熟练的人不会产生困惑;)

Nick写道:

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

这应该(有点)实际上是:

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

一些注意事项:

  1. 在最初的答案中,DoSomeAsyncWork() 实际上返回一个“可等待”的对象(Task),这一点并不明显;显然,你的 IDE 和编译器会很快告诉你方法实际上返回了什么……但是人类程序员不能仅仅看一眼就知道,这就是为什么……
  2. 微软已经创建/推荐将“Async”后缀添加到方法的“自然”名称中。注意:这不是必需的,而是强烈建议人类开发者更清楚地看到 API 提供的意图(而无需)
  3. 先前提供的 lambda 本质上是同步的,即使假定嵌套方法返回的是一个 Task。Lambda 可能会让人感到困惑,因为你需要消除 Lambda 带来的工作量(以运行时效率为代价),并手动验证/理解 DoSomeAsyncWork() 的返回类型——这也说明了 MS 对注释 #2 的理由。MS 还建议始终“观察”(要么“等待”,要么保留引用,要么“丢弃”)任何方法返回的任务(有一个静态分析器 NuGet 包可以查找异步等待反模式,可以帮助处理所有这些问题)。
  4. Lambda 中的“async”修饰符允许“等待”异步(返回 Task 的)方法。
  5. ConfigureAwait(false)……https://devblogs.microsoft.com/dotnet/configureawait-faq/

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接