**解释在各种场景中使用等待时的程序执行流**
本文原文
前言
这个文档解释了 Async/Await 是如何工作的。这可能是一个令人困惑的话题。我们将从一些简单的基本概念开始,然后慢慢地向更高级的概念发展。希望这些可视化图表能够帮助那些学习者。
下面的讨论主要是从 WPF 的角度出发,尽管我偶尔也会关注一下 WinForms。
目录
- 前言
- 命名规则
- 同步方法
- 异步方法
- 同步调用
- 异步调用
- 同步调用同步
- 常规调用
- 读取流常规调用
- 异步等待异步
- await stream.ReadAsync()
- ButtonClick调用 FooAsync ,FooAsync调用读取流
- 同步调用 Task. Run ()
- Sync → Task.Run() → Sync
- Sync → Task.Run() → Sync and Waiting
- 异步中等待Task.Run()
- 异步中等待Task.Run()任务,Task.Run()任务中运行同步方法
- 异步中等待Task.Run()任务,Task.Run()任务中运行异步方法
- 异步调用同步
- 同步调用异步=☠
- Sync → Async
- Sync → Async → .Wait() ☠
- Sync → Task.Run() → Async
- UI Thread → Task.Run() → Async → .Wait()
- ThreadPool → Task.Run() → Async → .Wait ☠
- 返回值
- 同步调用同步返回值
- 异步等待异步返回值
- Task.Run() 返回值
- 异步等待 Task.Run ()返回值
- 同步调用异步返回值
- 同步调用Task.Run()运行异步任务并等待其返回值
- 异步调用同步返回值
- 传递参数
- 在任意线程上完成
- 异步调用异步在任意线程上完成
- Task.Run()使用 CancelationToken
- 回到 UI 线程
- 消息队列
- 消息循环
- WPF 中的消息循环
- 回到 UI 线程
- WinForms
- WPF
- SynchronizationContext
- Await 如何运作
- 异步、方法签名和接口
- 正确使用Async/Await
- 将代码转换为异步
- 重写已有异步的代码
- 参考文献
命名规则
同步方法
同步(sync)方法是一种常规方法,它没有标记为异步,也没有等待。例如:
private void Foo()
{
...
}
异步方法
一个异步方法是一个被标记为异步并且在其中有一个等待的方法。例如:
private async Task FooAsync()
{
await BarAsync();
}
异步方法名称通常以“ … Async ()”结尾。
一个异步方法应该返回一个 Task。异步方法返回 void 唯一可以的地方是在事件处理程序中,例如按钮单击事件处理程序:
private async void Button_Click(object sender, RoutedEventArgs e)
{
await FooAsync();
}
同步调用
Bar();
OR
int x = Bar();
异步调用
异步调用是使用 wait 的调用。它可能返回一个值,也可能不返回。例如:
await BarAsync();
//OR:
int x = await BarAsync();
注意,等待并没有“启动”对 BarAsync ()的调用; 相反,等待决定了对 BarAsync ()的结果做了什么; 结果可能是一个不完整的任务,也可能是一个已完成的任务。
下面的陈述可以作为类比:
return Bar();
在这里,我们不说返回值‘启动’Bar () ; 相反,返回值决定执行 Bar ()的结果。
同步调用同步
常规调用
Foo ()调用 Bar () ,Bar ()运行,然后返回到 Foo ()。
读取流常规调用
Foo ()调用 读取stream。线程等待 stream.Read ()完成,然后继续。
异步等待异步
await stream.ReadAsync()
ButtonClick ()调用await stream.ReadAsync()。ReadAsync ()线程不再等待读取完成,而是返回给 ButtonClick ()的调用者,允许线程做其他事情。
通常,ButtonClick ()从 UI 线程的 Message Loop (如下所述)调用,并在 UI 线程上运行。通过在等待期间返回,UI 线程能够处理 MessageQueue 中的其他消息并更新屏幕。
稍后,当 stream. ReadAsync ()完成时,ButtonClick ()方法的其余部分将在 UI 线程上运行(深蓝色)。
如何让 UI 线程在流之后运行 ButtonClick ()的其余部分。ReadAsync ()完成有点复杂,稍后将解释。
一线程同时做两件事
在上面的场景中,ButtonClick ()调用等待流。ReadAsync ()UI 线程没有等待读取完成,而是返回给 ButtonClick ()的调用者,允许 UI 线程做其他事情。
此时此刻,可以说我们同时在做两件事:
1、我们正在等待 ReadAsync ()调用完成;
2、UI 线程正在处理消息队列中的消息;
等待实际上是否算做某事(通常不算) ,这是一个语义问题。我们确实在等待,并且我们已经设置了一些东西,以便在等待流之后,当这个等待完成代码的其余部分时。ReadAsync ()将执行; 但是,它是一个被动等待,并且我们不会在这个等待期间占用 UI 线程。UI 线程可以自由地做其他事情,同时我们也被动地等待等待流。ReadAsync ()完成。请注意,仍然只有一个线程,即 UI 线程,并且该线程仍然在做所有的工作。
ButtonClick调用 FooAsync ,FooAsync调用读取流
ButtonClick ()调用等待 FooAsync ()。FooAsync ()调用等待流。ReadAsync ()未完成的 Task 将返回给 FooAsync ()的调用方,而不是等待读取完成。Wait FooAsync ()发现返回给它的 Task 不完整,因此它返回给它的调用者,即 UI 线程的 Message Loop。这允许 UI 线程处理消息队列中的其他消息并更新屏幕。
稍后,当 ReadAsync ()完成时,FooAsync ()的其余部分将运行(深蓝色)。当 FooAsync ()到达终点时,它返回一个已完成的 Task,等待 FooAsync ()和 ButtonClick ()的其余部分运行。
本例中的所有操作都发生在 UI 线程上。UI 线程在等待读取完成时被释放。读取完成后,FooAsync ()的其余部分在 UI 线程上运行,当 FooAsync ()返回到 ButtonClick ()时,ButtonClick ()的其余部分在 UI 线程上运行。如何实现这一点的细节将在后面解释。
同步调用 Task. Run ()
Sync → Task.Run() → Sync
Foo ()中Task.Run(), Bar ()在 ThreadPool 线程上运行。Foo ()在不等待 Bar ()完成的情况下继续执行。Bar ()在 ThreadPool 线程上独立运行。
Sync → Task.Run() → Sync and Waiting
在 ThreadPool 线程上运行的 Task. Run ()队列 Bar ()。
Foo ()等待任务 t 完成。
运行 Foo ()的线程通过将其执行状态设置为“ WaitSleepjoin”(阻塞状态)进入等待状态,并生成其余的处理器时间片。(这使 CPU 可以运行其他线程。)线程在其阻塞条件得到满足之前不会消耗处理器时间。
稍后,当 Bar ()完成时,运行 Foo ()的线程将其执行状态设置回“ Run”,并在线程管理器有可用的时间片时恢复运行。
执行一个t.wait(),在 UI 线程上等待()是不明智的,因为这可能使程序无响应。我们不希望绑定 UI 线程什么都不做。(考虑将 Foo ()转换为异步方法并使用await Task.Run(()=>Bar())。
如果 Foo ()在 ThreadPool 线程上运行,则执行t.Wait ()是不明智,因为现在我们正在阻塞一个 ThreadPool 线程,等待另一个 ThreadPool 线程运行 Bar ()。为什么要开始另一个线程,然后等待它完成,而你本可以自己做这项工作?
这给我们带来了一个关于等待任务完成的一般规则:
避免使用 Task.Wait 和 Task.Result
“There are very few ways to use Task.Result and Task.Wait correctly so the general advice is to completely avoid using them in your code.”
David Fowler, Partner Software Architect at Microsoft
异步中等待Task.Run()
异步中等待Task.Run()任务,Task.Run()任务中运行同步方法
1、FooAsync ()方法中,Bar ()在 ThreadPool 线程上运行并返回给其调用者。(如果 FooAsync ()在 UI 线程上运行,则 UI 线程没有被阻塞,这是好事。)
2、Bar ()执行流(红色)。
3、当 Bar ()完成时,运行 Bar ()的任务完成。FooAsync ()然后继续使用它启动的同步上下文(蓝色)。这意味着如果 FooAsync ()在 UI 线程上运行,那么 FooAsync ()将继续在 UI 线程上运行; 如果 FooAsync ()在 ThreadPool 线程上运行,那么 FooAsync ()将继续使用任何 ThreadPool 线程。
注意 FooAsync ()不等待方法 Bar ()完成,FooAsync ()等待运行 Bar ()的任务完成。
异步中等待Task.Run()任务,Task.Run()任务中运行异步方法
1、await Task.Run(async () => await BarAsync())。在 ThreadPool 线程队列上运行 BarAsync ()并返回给其调用者。(如果 FooAsync ()在 UI 线程上运行,则 UI 线程没有被阻塞,这是好事。)
2、BarAsync ()执行流(红色)。
3、当 BarAsync ()到达等待流时。ReadAsync (buffer)语句,而不是等待读完,BarAsync ()返回并释放运行 BarAsync ()的 ThreadPool 线程以运行其他任务。
4、当 stream. ReadAsync (buffer)完成时,BarAsync ()的其余部分将在任何可用的 ThreadPool 线程上运行。
5、当方法 BarAsync ()完成时,运行 BarAsync ()的任务完成,其余的 FooAsync ()继续使用它启动时相同的 SynchronizationContext (蓝色)。这意味着如果 FooAsync ()在 UI 线程上运行,那么 FooAsync ()将继续在 UI 线程上运行; 如果 FooAsync ()在 ThreadPool 线程上运行,那么 FooAsync ()的其余部分将继续使用任何 ThreadPool 线程。
注意 FooAsync ()不等待 BarAsync ()方法完成,FooAsync ()等待运行 BarAsync ()的任务完成。
异步调用同步
通常,异步方法可以调用同步方法。异步方法可以暂且假设它是一个调用同步方法的同步方法。例如,异步代码可以调用一个简单的同步函数,该函数将两个数相乘并返回结果。
在有些情况下,使用 wait 来异步调用返回 Task 的同步方法可能会带来麻烦。还有一些情况下,这是完全可以接受的。它取决于返回的 Task 的详细信息。这个问题将在关于异步/等待的后续文章中进一步讨论。
同步调用异步=☠
术语“ Sync over Async”指的是调用异步代码的同步代码。同步代码不能等待异步代码,因此很难知道异步代码何时完成。更糟糕的是,在某些情况下,等待异步代码完成可能会导致死锁。这就引出了同步代码调用异步代码的一般规则:
异步调用同步很糟糕,不要这样做
下面,我们将研究在尝试从同步代码调用异步代码时可能发生的危险。
Sync → Async
同步 Foo ()方法调用 BarAsync ()。当 BarAsync ()调用等待流。 ReadAsync (buffer) ,它返回到 Foo ()继续执行。
稍后,在执行流之后。 ReadAsync (buffer)完成后,BarAsync ()的其余部分运行(深蓝色)。
注意,我们不能等待对 BarAsync ()的调用,因为 Foo ()是一个不支持等待的同步方法。我们无法知道 BarAsync ()的其余部分何时或是否运行。它可能永远不会运行,我们永远不会知道它。
Sync → Async → .Wait() ☠
警告: 可能导致死锁
1、在 UI 线程上运行的 ButtonClick ()调用 BarAsync ()。
2、BarAsync ()调用等待流。ReadAsync (buffer) ,它在某个时刻返回一个未完成的任务,该任务存储为 ButtonClick ()中的 Task t。
3、ButtonClick ()然后调用 t.Wait ()。 UI 线程现在被绑定,等待任务 t 完成。
4、稍后,当 ReadAsync() 完成时,它将 BarAsync ()的其余部分排队,以便在 UI 线程上运行。不幸的是,ButtonClick ()正在占用 UI 线程,等待 BarAsync ()完成。**这将导致死锁:ButtonClick ()正在等待 BarAsync () ,而 BarAsync ()正在等待 ButtonClick ()。**由于 ButtonClick ()阻塞了 UI 线程,整个程序冻结,无法响应键盘按键或鼠标单击。
注意,在某些情况下代码可能不会死锁: 如果stream.ReadAsync() 被 Task.Delay(0)替换,await将跳过时间消耗返回一个未完成的任务给调用者”,只是继续运行。但是,如果Task.Delay(0) 被Task.Yield()代替。那么代码仍导致死锁。
让我们看看当我们试图通过使用 Task.Run ()调用异步方法来修复这个问题时会发生什么。
Sync → Task.Run() → Async
在 UI 线程上运行的 ButtonClick ()会创建运行 BarAsync ()的任务 t。然后 ButtonClick ()继续执行,不再进一步检查任务 t。
另外,任务 t 在 ThreadPool 线程上运行 BarAsync ()。(任务从 Task 开始。在 ThreadPool 线程上运行 Run ()当 BarAsync ()到达等待流时。ReadAsync (buffer)返回释放 ThreadPool 线程,以便线程可以处理其他事情。
稍后,当 ReadAsync (buffer)完成时,BarAsync ()的其余部分将在任何可用的 ThreadPool 线程上运行。它之所以可以在任何可用的 ThreadPool 线程上完成,是因为从理论上讲,所有 ThreadPool 线程都是相同的。(一个更技术性的解释是,因为 ThreadPool 线程没有SynchronizationContext,所以使用了默认的SynchronizationContext,即“ 任意ThreadPool 线程”
我们仍然存在不知道异步任务何时完成的问题。让我们看看如果我们引入一个t.Wait ()等待任务完成。
UI Thread → Task.Run() → Async → .Wait()
在 UI 线程上运行的同步ButtonClick()方式使用 Task.Run ()创建运行 BarAsync ()的任务 t。ButtonClick ()然后调用 t.Wait ()并等待任务 t 完成,从而阻塞 UI 线程。
同时,任务 t 在 ThreadPool 线程上运行 BarAsync ()。(任务从 Task 开始,在 ThreadPool 线程上运行 Run ()当 BarAsync ()到达等待流时。ReadAsync (buffer) ,它返回释放 ThreadPool 线程来处理其他事情。
稍后,当 ReadAsync (buffer)完成时,BarAsync ()的其余部分将在任何可用的 ThreadPool 线程上运行。
当方法 BarAsync 完成时,运行 BarAsync 的任务 t 完成,t.Wait ()完成,ButtonClick 的其余部分继续在 UI 线程上运行。
尽管这不会死锁,但它确实用.Wait () ,使我们的程序在等待时对用户输入无响应。将 ButtonClick ()转换为一个异步方法并等待任务 t 会更好。
ThreadPool → Task.Run() → Async → .Wait ☠
假设 Foo ()在 ThreadPool 线程上运行。Foo ()调用 Task.Run ()创建一个运行 BarAsync ()的任务t。然后Foo()调用t.Wait ()并等待任务 t 完成,从而阻塞正在运行的 ThreadPool 线程。
同时,任务 t 在另一个 ThreadPool 线程上运行 BarAsync ()。(任务使用 Task.Run(),任务是在 ThreadPool 线程上运行),当 BarAsync ()到达await stream.ReadAsync(buffer),它返回释放 ThreadPool 线程来处理其他事情。
稍后,当 ReadAsync (buffer)完成时,BarAsync()方法的其余部分将在任何可用的 ThreadPool 线程上运行。
线程池饥饿
这里的潜在问题是Foo()阻塞了一个 ThreadPool 线程,在等待之后,我们需要另一个 ThreadPool 线程来完成 BarAsync ()。我们可以想象这样一个场景:启动多个Foo()实例绑定ThreadPool 线程,直到没有更多的 ThreadPool 线程为止。所有 ThreadPool 线程都被阻塞,等待另一个ThreadPool线程完成运行 BarAsync ()。
此时,操作系统发现需要更多的 ThreadPool 线程,因此它创建了一个新的 ThreadPool 线程。这个新的 ThreadPool 线程可以运行 BarAsync ()的其余部分。或者,它可以运行 Foo ()的另一个实例,新的 ThreadPool 线程运行的方法取决于程序细节以及如何管理 ThreadPool 队列。如果新的 ThreadPool 线程总是运行 BarAsync()的其余部分,系统将开始恢复; 然而,如果新的 ThreadPool 线程总是运行 Foo ()的另一个实例,那么我们注定要失败: Foo()将阻塞新的 ThreadPool 线程,我们将回到我们的 ThreadPool 饥饿状态,除非 ThreadPool 的大小增加。系统可能永远不会恢复,因为 ThreadPool 的大小会无限期地缓慢增加,所有 ThreadPool线程都会被阻塞,每个线程都会永远等待另一个 ThreadPool 线程来恢复。
在这个链接中可以看到这种类型的 ThreadPool 饥饿的一个例子。
ThreadPool 饥饿
返回值
同步调用同步返回值
int x = Bar();
异步等待异步返回值
int x = await BarAsync();
这是调用异步方法的正常方式:
1、FooAsync ()调用 BarAsync ()。
2、BarAsync ()遇到等待任务。延迟(2000) ; 并将未完成的任务返回给 FooAsync () ,FooAsync 将未完成的任务返回给调用者。
3、BarAsync ()完成并将7返回给 FooAsync (),FooAsync ()将7存储在变量 x 中。
4、FooAsync ()现在继续运行,因为它有一个x值。
Task.Run() 返回值
Sync 调用 Task.Run()等待 Sync方法返回值
Task<int> t = new Task<int>(Bar);
t.Start();
t.Wait();
int x = t.Result;
OR
Task<int> t = Task.Run( () => Bar() );
t.Wait();
int x = t.Result;
OR:
Task<int> t = Task.Run( () => Bar() );
int x = t.Result;
OR:
int x = Task.Run( () => Bar() ).Result;
异步等待 Task.Run ()返回值
int x = 启动Task.Run ()在同步方法上获取返回值
这是等待耗时的同步代码的标准方法。
1、FooAsync()使Bar ()在 ThreadPool 线程队列上运行并返回给其调用者。(如果 FooAsync ()在 UI 线程上运行,则 UI 线程没有被阻塞,这是好事。)
2、Bar ()执行流(红色)。
3、当 Bar ()完成时,运行 Bar ()的任务完成,并且7存储在变量 x 中。
4、FooAsync ()然后继续: 如果 FooAsync ()在 UI 线程上运行,那么 FooAsync ()在 UI 线程上继续; 如果 FooAsync ()在 ThreadPool 线程上运行,那么 FooAsync ()在任何 ThreadPool 线程上继续。
int x = 启动Task.Run ()在异步方法上获取返回值
1、int x = await Task.Run(async () => await BarAsync())等待任务。在 PoolThread 线程队列上运行 BarAsync ()并返回给它的调用者。(如果 FooAsync()在 UI 线程上运行,则 UI 线程没有被阻塞,这是好事。)
2、BarAsync ()执行流(红色)。
3、当 BarAsync ()到达等待流时。ReadAsync (buffer)语句,而不是等待读完,BarAsync ()返回并释放运行 BarAsync ()的 ThreadPool 线程以运行其他任务。
4、当 stream. ReadAsync (buffer)完成时,BarAsync ()的其余部分将在任何可用的 ThreadPool 线程上运行。
5、当方法 BarAsync ()完成时,运行 BarAsync ()的任务完成,并且7存储在变量 x 中。
6、FooAsync ()继续执行: 如果FooAsync ()在 UI 线程上运行,那么 FooAsync()继续在 UI 线程上运行;如果 FooAsync()在 ThreadPool 线程上运行,那么 FooAsync()继续在任何可用的 ThreadPool 线程上运行。
在这种情况下,可以考虑删除 Task.Run(),只使用int x = await BarAsync();
await BarAsync()对比 await Task.Run(async () => await BarAsync())
await BarAsync()
直接运行 BarAsync (),如果 BarAsync()需要一些时间并且不等待耗时的代码(例如,它可能正在做一些 CPU 密集型的计算,所以它不能等待,因为它不是在等待,而是在工作) ,那么调用者必须等待 BarAsync ()完成耗时的工作。
await Task.Run(async ()=> await BarAsync())
这里,我们正在等待运行BarAsync 的 Task。这使调用者在等待任务完成时可以做其他事情。该任务在后台 ThreadPool 线程上运行
同步调用异步返回值
这是无法执行的,因为 BarAsync()返回的是 Task < int > ,而不是 int。
private async Task<int> BarAsync()
{
await Task.Delay(2000);
return 7;
}
<s>int x = BarAsync();</s> "Cannot implicitly convert Task<int> to int."
调用方需要是一个异步方法。
同步调用Task.Run()运行异步任务并等待其返回值
会导致死锁! (见线程池饥饿)
不要在异步任务上等待。相反,应该等待一个异步任务(或者删除 Task.Run ()并等待方法)。
异步调用同步返回值
只要Bar()等待完成返回的不是Task,这通常是允许的。有时候这可能会导致麻烦。有关这种情况的详细信息将在另一篇有关异步/等待的后续文章中进行讨论。
传递参数
int x = await Task.Run(() => Bar(a, b, c));
int x = await Task.Run(async () => await BarAsync(a, b, c));
还可以这样做:
int x = Task.Run(() => Bar(a, b, c)).Result;
不过在这种情况下,可以省略任务,并这样做:
int x = Bar(a, b, c);
不要等待异步方法或任务。
不要这样做:
int x = Task.Run(async () => await BarAsync(a, b, c)).Result;
因为这可能导致线程池饥饿,死锁。
在任意线程上完成
添加.ConfigureAwait (false)允许在任何可用线程(深红色)上运行等待之后继续执行。通常,这将是一个 ThreadPool 线程。当我们知道ButtonClick()的其余部分不需要在 UI 线程上运行时,这样做是很方便的。
两个线程不能同时做两件事
在上面的示例中,由于.ConfigureAwait (false)允许ButtonClick()的其余部分在后台ThreadPool 线程上运行,而开始运行 ButtonClick()的 UI 线程可以自由地做其他事情。然而,我们并没有同时做两件事情,因为即使 ButtonClick()的其余部分运行在不同的线程上,它仍然要等到调用 ReadAsync(buffer)完成之后才能运行。我们永远不会同时做两件事情,即使我们使用两个线程。
异步调用异步在任意线程上完成
1、ButtonClick()在 UI 线程上启动。
2、ButtonClick()调用FooAsync()并等待其完成。
3、FooAsync()调用await stream.ReadAsync()。
4、ReadAsync ()将未完成的任务返回给 ButtonClick () ,而ButtonClick()又返回给其调用者(消息循环)。
5、当 stream.ReadAsync()完成后,FooAsync ()的其余部分将在任何可用线程上运行,由于使用了.ConfigureAwait (false) ; (这是一种简短的说法。Configurewait (ContineOnCapturedContext: false) 😉。
6、FooAsync ()完成并返回到 ButtonClick ()。
7、ButtonClick()的其余部分在UI 线程上运行(因为对 FooAsync ()的调用没有附加.ConfigureAwait (false))。
Task.Run()使用 CancelationToken
因为我有很多使用 Task.Run()的例子,用一个在 Task 中使用CcellationToken 的示例来演示Task.Run()比较好。
来自 Microsoft 文档:
EN:“When the owning object calls CancellationTokenSource.Cancel(), the IsCancellationRequested property on every copy of the cancellation token is set to true.”
CHN:”当拥有对象调用CancellationTokenSource.Cancel(),每个取消令牌副本上的IsCancellationRequested属性都设置为 true。”
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
CancellationTokenSource cts;
Task<int> task;
private async void Button_Click(object sender, RoutedEventArgs e)
{
this.cts?.Cancel();
this.cts = new CancellationTokenSource();
this.task = Task.Run(() => SomeTask(cts.Token), cts.Token);
int x = await this.task;
this.task.Dispose();
this.task = null;
}
private async Task<int> SomeTask(CancellationToken ct)
{
for(int i=0; i < 20; ++i)
{
ct.ThrowIfCancellationRequested();
await Task.Delay(500);
}
return 5;
}
1、注意如何将取消令牌两次传递给 Task.Run (),它作为一个参数传递给 Some Task (cts. Token)。将取消令牌传递给 Some Task (),可以让 Some Task()定期检查令牌的状态,看看它是否被设置为“ Cancel”状态,以便在设置为“ Cancel”状态时可以中止过程。
2、它作为第二个参数传递给 Task. Run (… ,cts. Token);将取消令牌作为第2个参数传递给 Task.Run ()Task.Run()被调用时,如果“取消令牌”在执行任务时已设置为“取消”,则Task.Run()可跳过运行“SomeTask()”。
请注意,Microsoft 开发人员 Stephen Toub 认为不需要处理 Tasks,因此处理 Task 的上述代码可能有些过分
回到 UI 线程
为了理解当需要时如何等待返回到 UI 线程,我们需要解释消息队列、消息循环和 SynchronizationContext 的概念。
消息队列
具有 GUI (图形用户界面)的 Windows 程序对于创建窗口的每个线程都有一个单独的消息队列。通常,只有初始线程创建窗口并维护程序的消息队列。这个线程被称为“ UI 线程”(用户界面线程)或 GUI 线程(图形用户界面线程)。(它们是一回事)
事件:(如按钮单击或键盘按键)被放置到此消息队列中。
下面是消息队列的可视图。这被称为“先进先出”(FIFO)队列。按照消息进入的顺序检索消息。假设每个蓝色矩形都是一个“消息”,例如一个 Button Click 事件消息,或者一个“ Keyboard Key was Press Down”消息。
消息队列中的事件包括:
搜索文件 WinUser.h 以查看 WM _ * 消息的完整列表。
Windows 操作系统负责将事件消息发送到消息队列。它确定桌面上的哪个窗口具有焦点,并将消息发送到与创建该窗口的线程关联的消息队列。
有关消息及消息队列的详情,请按此处
Posting a Message
将消息排队到消息队列称为“发布”消息。
消息通过调用操作系统的 PostMessage ()添加到消息队列中。
消息循环
UI线程运行称为消息循环(也称为消息泵)的代码。这是从消息队列中删除消息并处理它们的代码。这段代码是一个无止境的循环,在程序执行期间运行,看起来像这样:
while(frame.Continue)
{
if (!GetMessage(ref msg, IntPtr.Zero, 0, 0))
break;
TranslateAndDispatchMessage(ref msg);
}
如果消息队列中没有消息,则 GetMessage ()将阻塞; 也就是说,GetMessage ()将等待消息返回,然后返回消息。
直到程序被请求终止,这个无止境的循环才会退出。
PowerPoint Presentation on Windows and Messages
提示:The WM_TIMER Message
WM _ TIMER 消息以非“先进先出”(FIFO)的特殊方式处理。SetTimer 系统函数设置一个系统计时器,计数器包含适当数量的“刻度”,直到计时器过期。在每个硬件计时器滴答,Windows 递减计数器的计数器。当计数器达到0时,Windows 将在适当应用程序的消息队列头中设置一个计时器过期标志。当消息循环调用 GetMessage时,如果消息队列为空,它将检查计时器过期标志,如果设置了该标志,函数将返回 WM _ TIMER 消息并重置标志。这意味着,如果 CPU 忙,并且在清空消息队列时有一个延迟,则 WM _ TIMER 消息可能会被延迟。这也意味着多个 WM _ TIMER 消息不能“堆积”多个计时器可能会过期,多次设置计时器过期标志,但是在重新设置标志之前只会生成一条 WM _ TIMER 消息。GetMessage也有可能在另一个时间段到期之前返回一个 WM _ TIMER 消息,然后在第一个消息到期之后立即返回第二个 WM _ TIMER 消息。
WPF 中的消息循环
每个 WPF 程序都有一个Main()入口点,可以通过在 Visual Studio 的解决方案资源管理器中的 App.xaml 下查看并选择 Main():
代码可能是显式的,或者在本例中是隐式的自动生成的(因此在文件名 App.g.i.cs 中有‘ g’)。
UI 线程是运行这个Main ()方法的线程。Main()创建类 App 的一个实例,该实例依次指定要创建的窗口 MainWindow。
Main()方法然后调用 app.InitializeComponent (),最后调用 app.Run () ; 这是消息循环所在的位置。方法Main ()在程序存在之前不会返回。它的生命周期是作为运行消息循环的 UI 线程。
App类应用程序继承自 System.Windows.Application, System.Windows.Application 的代码可以在此链接上查看
找到 Run()方法并将其跟踪到消息循环。
Run → RunInternal → RunDispatcher → Dispatcher.Run() → PushFrame → dispatcher.PushFrame → message loop.
回到 UI 线程
WinForms
在 WinForms 中,后台线程可以启动要在 UI 线程上运行的代码,代码如下:
control.BeginInvoke(delegate)
其中的委托是我们要在 UI 线程上执行的代码。这将委托发送到创建控件的线程(通常是 UI 线程)的消息队列。
WPF
在 WPF 中,后台线程可以通过以下方法启动要在 UI 线程,代码如下:
Dispatcher.CurrentDispatcher.BeginInvoke(delegate)
其中的委托是我们要在 UI 线程上执行的代码。委托被发送到 Dispatcher,并最终在其中运行。(Dispatcher是线程和消息队列的组合。通常,线程是程序的 UI 线程)。
SynchronizationContext
这可以确定我们是否在 UI 线程上。
SynchronizationContext允许代码返回UI线程,如果我们在输入等待时正在UI线程上运行。
在 WPF中,SynchronzationContext有两个类型:
1、SynchronizationContext有一个Post()方法,它将方法排队在 ThreadPool 线程上运行。(这个基类实际上没有使用。它可能是早期设计迭代中遗留下来的。)
2、DispatcherSynchronizationContext 继承自SynchronizationContext,并用一个方法重写Post(),该方法在UI线程上以队列方式排队运行。
要使用SynchronizationContext,可以调SynchronizationContext.Current:
代码如下:
SynchronizationContext sc = SynchronizationContext.Current;
如果我们当前在 UI 线程上,那么对于 WPF 项目,这将返回 DispatcherSynchronizationContext 的实例。(类似地,对于 WinForms 项目,这将返回 WindowsFormsSynchronizationContext 的一个实例,该实例也继承自 SynchronizationContext。)另一方面,如果我们在一个 ThreadPool线程上,那么SynchronizationContext.Current 返回 null。
DispatcherSynchronizationContext 的代码
DispatcherSynchronizationContext的Post()如下:
public override void Post(SendOrPostCallback d, Object state)
{
_dispatcher.BeginInvoke(_priority, d, state);
}
DispatcherSynchronizationContext的构造函数把Dispatcher. CurrentDispatcher赋值给_dispatcher。
Await 如何运作
我们已经讨论了UI 线程如何处理消息队列和消息循环,以及SynchronizationContext如何将我们带回 UI 线程,现在我们可以回答 await如何工作的问题。
当我们awai时,如果我们在 UI 线程上,那么我们希望稍后在等待之后恢复在UI 线程上。另一情况,如果我们在进入等待时是在一个 ThreadPool 线程上,那么我们希望在等待之后恢复在一个 ThreadPool 线程上。
现在考虑以下代码:
private async void ButtonClick()
{
await stream.ReadAsyc(this.buffer);
UpdateGUI(this.buffer);
}
在这里,从 UI 线程调用ButtonClick()。该方法首先调用ReadAsync()将一些数据提取到 this.buffer 中。这种读取需要很长时间,因此ReadAsync()返回一个未完成的Task,等待它返回给 ButtonClick()的调用者,ButtonClick()是消息循环。这将释放 UI 线程来处理消息循环中的其他消息。
稍后,在ReadAsync()完成之后,我们希望恢复在 UI 线程上运行 ButtonClick()的其余部分。
因此,当编译器遇到 await关键字时,它会在调用stream.ReadAsync(this.buffer)之前生成代码用来捕获 SynchronizationContext。代码如下所示:
SynchronizationContext sc = SynchronizationContext.Current;
在运行时,如果我们在 UI 线程上,那么 sc 就成为 DispatcherSynchronizationContext 的一个实例(它从 SynchronizationContext 继承,所以它是一个 SynchronizationContext) ; 否则,如果我们在一个 ThreadPool 线程上,那么 sc就被设置为 null。
等待之后,编译器生成如下代码:
if (sc == null)
RestOfMethod();
else
sc.Post(delegate { RestOfMethod(); }, null);
这可以解释为,“如果我们没有 SynchronizationContext,那么只要使用我们正好所在的线程运行剩余的代码,否则,使用了 SynchronizationContext则回到 UI 线程。”
等待的后续是如何运行的
还有一些关于await 关键字的内容我没有在这里解释。当编译器遇到await 关键字时,它创建一个状态机来处理将代码分解为多个部分的所有细节: 等待之前的部分和等待之后的部分。可以想象,这会有点混乱,我很乐意让编译器处理这些细节。如果读者感兴趣的话,互联网上有许多优秀的文章解释了这个状态机背后的细节。
异步、方法签名和接口
摘自 Alex Davies 的《 C # 5.0异步》一书
与公共关键字或静态关键字一样,异步关键字也出现在方法的声明中。尽管如此,就覆盖其他方法、实现接口或被调用而言,异步并不是方法签名的一部分。
与应用于方法的其他关键字不同,async 关键字的唯一效果是对应用它的方法的编译,这些关键字改变了它与外部世界的交互方式。正因为如此,关于重写方法和实现接口的规则完全忽略了异步关键字。
class BaseClass
{
public virtual async Task<int> AlexsMethod()
{
...
}
}
class SubClass : BaseClass
{
// This overrides AlexsMethod above
public override Task<int> AlexsMethod()
{
...
}
}
接口不能在方法声明中使用异步,这仅仅是因为没有必要。如果接口要求方法返回 Task,则实现可以选择使用异步,但是否使用异步是实现方法的选择。接口不需要指定是否使用异步。
方法返回 Task 的一个问题是,该方法有时希望通过同步任务等待 Task。Wait () ,而其他时候该方法期望通过异步等待任务await task; 在任务上使用错误的等待类型会导致死锁和其他问题。这将在另一篇后续文章中进一步讨论。
正确使用Async/Await
不要阻塞异步代码
当调用 Async 时,名称为Async 的方法应该在它的前面等待。异步方法最终需要调用异步 I/O 例程。
首先是.NET Framework 4.5,I/O 类型包括异步方法来简化异步操作。一个异步方法在其名称中包含 Async,例如 ReadAsync ()、WriteAsync ()、 CopyToAsync ()、FlushAsync()、 ReadLineAsync ()和 ReadToEndAsync ()。这些异步方法在流类(如 Stream、 FileStream 和 MemoryStream)和用于读取或写入流的类(如 TextReader 和 TextWriter)上实现。
除非整个调用堆栈都是异步的,否则异步的努力毫无意义。(所有异步方法,从异步按钮单击事件处理程序开始,一直到异步 I/O 系统调用。)
如果代码中没有本地异步方法,那么就没有理由将任何内容转换为异步,最终只会在代码中的某个地方出现“异步优先于同步”或“同步优先于异步”。也就是说,在某个地方,异步方法将调用同步方法,或者同步方法将调用异步方法。
另外,不要混合 List < T > 。使用异步方法(或者实际上是并行方法)。对于每个都有完全相同的问题)。参见: C # 异步反模式: 反模式 # 5: 使用异步方法混合 ForEach。
https://markheath.net/post/async-antipatterns
将代码转换为异步
1、确定一个可以更改为异步 I/O 调用的本机 I/O 调用,例如,Read ()→ ReadAsync ()。
2、将本机 I/O 调用转换为异步 I/O 调用。[例如,转换 Read ()→ ReadAsync ()]将该方法指定为异步方法。
3、调用此异步方法的所有方法现在都需要转换为异步方法。(编译器可以帮助你,让它告诉你什么需要修复。)
4、重复上述最后一步,直到不再有需要转换为异步方法的方法为止。在到达事件处理程序(如按钮单击事件处理程序)之前,调用链上的所有方法都将转换为异步方法。
不要让同步方法调用异步方法
重写已有异步的代码
异步方法最终会调用本机异步 I/O 方法吗?比如 ReadAsync 或 WriteAsync?,异步方法调用的异步方法是否一直沿着调用链向上到达事件处理程序?是否有同步方法的实例调用异步方法(“ Sync over Async”) ?如果有,是否可以将同步方法更改为调用链上的异步方法?或者,可能不需要异步方法,可以将其转换为同步方法?
参考文献
Asynchronous Programming Guidance – David Fowler, Partner Software Architect at
Processes, Threads, and Jobs in the Windows Operating System by Kate Chase and Mark E. Russinovich 6/17/2009
Windows Internals, Part 1 (Developer Reference) by Pavel Yosifovich, Mark E. Russinovich, et al. | May 15, 2017
Windows Internals, Part 2 (7th Edition) (Developer Reference) by Mark E. Russinovich, Andrea Allievi, et al. | Jul 16, 2020