试图理解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.WhenAll
或 await Task.WhenAny
进行并发处理很容易,除非您显式使用线程池(例如,Task.Run
或 ConfigureAwait(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();
几点注意事项:
- 在最初的答案中,DoSomeAsyncWork() 实际上返回一个"可等待的"(一个任务,用于意图/目的)并不明显;显然,你的 IDE 和编译器会很快告诉你该方法实际上返回了什么......但是人类程序员不能只看就知道,这就是为什么......
- Microsoft创建/建议将"Async"的后缀附加到方法的"自然"名称中。注意:这不是必需的,强烈建议人类开发人员更清楚地了解 API 的意图(无需
- 之前提供的 lambda 本质上是同步的,即使假定从嵌套方法返回的任务是由 lambda 返回的。 lambda 可能会令人困惑 b/c 您需要取消 lambda 为您节省的工作(以运行时效率低下为代价)并使您的人类手动验证/理解 DoSomeAsyncWork() 的返回类型 - 这说明了 MS 对注释 #2 的基本原理。MS 还建议始终"观察"("等待"、保留引用或"丢弃")任何方法返回的任务(有一个静态分析器 NuGet 包来查找可以帮助解决所有这些问题的异步等待反模式)lamba 的"异步"修饰符允许异步(任务返回)方法的"等待"。
- ConfigureAwait(false)...https://devblogs.microsoft.com/dotnet/configureawait-faq/
我希望这对至少一个人有帮助。 我知道巴特勒很久以前就写@Nick他的答案,远在我完全理解这些东西之前。 我只是想为那些在 2023+ 中寻求它的人提供更好的指导/清晰度。