我强烈推荐阅读这篇2012年的博客文章,当时引入了
await
关键字,但它解释了控制台程序中异步代码的工作原理:
https://devblogs.microsoft.com/pfxteam/await-synchronizationcontext-and-console-apps/
作者随后表示,此关键字(await)将始终修改返回Task对象的方法。当逻辑流到达
await
标记时,调用线程在此方法中暂停,直到调用完成。如果运行此应用程序的版本,则会发现“已完成”消息显示在“完成工作!”消息之前。如果这是一个图形应用程序,则用户可以在
DoWorkAsync()
方法执行时继续使用UI。作者不够精确。我想要改变这个:当逻辑流到达
await
标记时,调用线程在此方法中暂停,直到调用完成。 改为这个:当逻辑流到达
await
标记(即在
DoWorkAsync
返回
Task
对象之后),函数的本地状态在内存中某处保存,并且运行线程执行
return
返回到异步调度程序(即线程池)。
我的观点是,
await
不会导致线程“挂起”(也不会导致线程阻塞)。
下一句话也有问题:
如果您运行此应用程序的版本,您会发现“已完成”消息显示在“完成工作!”消息之前(我认为作者指的是一个语法上相同但省略了await关键字的版本)。
提出的主张是不正确的。调用的方法DoWorkAsync仍然返回一个Task<String>,该字符串无法以有意义的方式传递给Console.WriteLine:必须首先await 返回的 Task<String>。
通过查阅有关async/await工作原理的各种在线文档和文章,我认为"await"会在遇到第一个"await"时起作用,程序会检查方法是否已经完成,如果没有,它将立即返回到调用方法,然后在可等待任务完成后返回。你的想法基本上是正确的。但是,如果调用方法本身是Main(),它会返回给谁呢?它会简单地等待await完成吗?这就是代码表现出等待5秒钟才打印“已完成”的原因吗?它返回到CLR维护的默认线程池。
每个CLR程序都有一个线程池,这就是为什么即使是最简单的.NET程序的进程也会在Windows任务管理器中显示4到10个线程计数的原因。然而,大多数线程将被挂起(但它们被挂起的事实与使用
async
/
await
无关)。
但这引出了下一个问题:因为
DoWorkAsync()
本身在这里调用了另一个
await
的方法,当遇到那个
await Task.Run()
行时,显然要等待5秒钟才能完成,那么
DoWorkAsync()
不应该立即返回给调用方法
Main()
吗?如果发生这种情况,
Main()
不应该继续打印“已完成”,就像书中作者建议的那样吗?
是和不是 :)
如果您查看编译后程序的原始CIL(MSIL),会有所帮助(
await
是一种纯语法特性,不依赖于.NET CLR的任何实质性更改,这就是为什么
async
/
await
关键字是在.NET Framework 4.5中引入的,即使.NET Framework 4.5运行在比它早3-4年的.NET 4.0 CLR上也是如此)。
首先,我需要将您的程序
语法重新排列成这样(这段代码看起来不同,但它编译为与原始程序相同的CIL(MSIL)):
static async Task Main(string[] args)
{
Console.WriteLine(" Fun With Async ===>");
Task<String> messageTask = DoWorkAsync();
String message = await messageTask;
Console.WriteLine( message );
Console.WriteLine( "Completed" );
Console.ReadLine();
}
static async Task<string> DoWorkAsync()
{
Task<String> threadTask = Task.Run( BlockingJob );
String value = await threadTask;
return value;
}
static String BlockingJob()
{
Thread.Sleep( 5000 );
return "Done with work!";
}
这是发生的事情:
CLR 加载您的程序集并找到 Main
入口点。
CLR 还使用从操作系统请求的线程填充默认线程池,它立即挂起这些线程(如果操作系统没有自己挂起它们-我忘记了这些细节)。
然后,CLR 选择一个线程作为主线程,另一个线程作为 GC 线程(这还有更多细节,我认为它甚至可能使用主要的操作系统提供的 CLR 入口点线程-我对这些细节不确定)。我们将其称为 Thread0
。
Thread0
然后运行 Console.WriteLine(" Fun With Async ===>");
作为普通方法调用。
Thread0
然后以普通方法调用的方式调用 DoWorkAsync()
。
Thread0
(在 DoWorkAsync
中)然后调用 Task.Run
,传递委托(函数指针)到 BlockingJob
。
- 请记住,
Task.Run
是 "安排(而不是立即运行)此委托在线程池中的一个线程上作为概念性的“作业”,并立即返回表示该作业状态的 Task<T>
的缩写。
- 例如,如果在调用
Task.Run
时线程池已经耗尽或繁忙,则根本不会运行 BlockingJob
,直到线程返回到池中-或者如果您手动增加池的大小。
Thread0
立即获得一个代表 BlockingJob
生命周期和完成情况的 Task<String>
。请注意,在此时,BlockingJob
方法可能已经运行,也可能还没有运行,因为这完全取决于您的调度程序。
Thread0
然后遇到 BlockingJob
的作业的 Task<String>
的第一个 await
。
- 此时,实际的 CIL(MSIL)为
DoWorkAsync
包含一个有效的 return
语句,它导致 真正的 执行返回到 Main
,然后立即返回到线程池,并让 .NET 异步调度程序开始担心调度。
因此,当 Thread0
返回到线程池时,BlockingJob
可能已经被调用,也可能没有被调用,这取决于计算机设置和环境(例如,如果您的计算机只有一个 CPU 核心,则会发生不同的事情-但还有其他很多事情!)。
- 完全有可能,
Task.Run
将 BlockingJob
作业放入调度程序,然后直到 Thread0
本身返回到线程池,调度程序才运行它,并且整个程序仅使用单线程。
- 但是
脚注
请记住,挂起的线程与阻塞的线程不是同一件事情:这是一个过于简化的说法,但在本答案中,“挂起的线程”具有空的调用堆栈,并且可以立即由调度程序投入使用以执行某些有用的操作,而“阻塞的线程”具有填充的调用堆栈,调度程序不能接触它或重新分配它,除非它返回到线程池——请注意,线程可能因为正在运行正常代码(例如while循环或自旋锁),因为被同步原语(如Semaphore.WaitOne)阻塞,因为通过Thread.Sleep休眠,或者因为调试器指示操作系统冻结线程而“阻塞”。
在我的回答中,我说C#编译器实际上会将每个await语句周围的代码编译成“子方法”(实际上是状态机),这就允许一个线程(任何线程,无论其调用堆栈状态如何)“恢复”一个方法,使其线程返回到线程池。这就是它的工作原理:
假设您有这个async方法:
async Task<String> FoobarAsync()
{
Task<Int32> task1 = GetInt32Async();
Int32 value1 = await task1;
Task<Double> task2 = GetDoubleAsync();
Double value2 = await task2;
String result = String.Format( "{0} {1}", value1, value2 );
return result;
}
编译器将生成CIL(MSIL),这与C#的概念相对应(即,如果不使用“async”和“await”关键字编写)。
(此代码省略了许多细节,如异常处理,状态的实际值,它内联了AsyncTaskMethodBuilder,捕获了this等等-但这些细节现在并不重要)
Task<String> FoobarAsync()
{
FoobarAsyncState state = new FoobarAsyncState();
state.state = 1;
state.task = new Task<String>();
state.MoveNext();
return state.task;
}
struct FoobarAsyncState
{
public Int32 state;
public Task<String> task;
Task<Int32> task1;
Int32 value1
Task<Double> task2;
Double value2;
String result;
public void MoveNext()
{
switch( this.state )
{
case 1:
this.task1 = GetInt32Async();
this.state = 2;
AwaitUnsafeOnCompleted( this.task1.GetAwaiter(), this );
return;
case 2:
this.value1 = this.task1.Result;
this.task2 = GetDoubleAsync();
this.state = 3;
AwaitUnsafeOnCompleted( this.task2.GetAwaiter(), this );
return;
case 3:
this.value2 = this.task2.Result;
this.result = String.Format( "{0} {1}", value1, value2 );
this.task.TrySetResult( this.result );
return;
}
}
}
请注意,FoobarAsyncState
是一个 struct
而不是一个 class
,出于性能原因,我不会深入讨论。
await
(正如其名称所示)等待任务完成,然后将在同一线程上创建一个继续执行的可能的延续(取决于同步上下文),以继续你所在的块的顺序执行。 - TheGeneralmain
是一个特殊情况,因为它是应用程序的入口点。 - TheGeneral