tha*_*ssd 3 .net c# async-await
我正在通过 Andrew Troelsen 的书“Pro C# 7 With .NET and .NET Core”学习 C#。在第 19 章(异步编程)中,作者使用了这些示例代码:
static async Task Main(string[] args)
{
Console.WriteLine(" Fun With Async ===>");
string message = await DoWorkAsync();
Console.WriteLine(message);
Console.WriteLine("Completed");
Console.ReadLine();
}
static async Task<string> DoWorkAsync()
{
return await Task.Run(() =>
{
Thread.Sleep(5_000);
return "Done with work!";
});
}
Run Code Online (Sandbox Code Playgroud)
作者接着说
"... this 关键字 (await) 将始终修改返回 Task 对象的方法。当逻辑流到达 await 标记时,调用线程将在此方法中挂起,直到调用完成。如果您要运行此版本在应用程序中,您会发现 Completed 消息显示在 Done with work! 消息之前。如果这是一个图形应用程序,用户可以在 DoWorkAsync() 方法执行时继续使用 UI”。
但是当我在 VS 中运行这段代码时,我没有得到这种行为。主线程实际上被阻塞了 5 秒,直到“完成工作!”之后才会显示“完成”。
查看有关 async/await 如何工作的各种在线文档和文章,我认为“await”会起作用,例如当遇到第一个“await”时,程序会检查该方法是否已经完成,如果没有,它会立即“ return”到调用方法,然后在等待任务完成后返回。
但是如果调用方法是 Main() 本身,它返回给谁?它会简单地等待等待完成吗?这就是代码行为正常的原因(在打印“已完成”之前等待 5 秒)?
但这引出了下一个问题:因为这里的 DoWorkAsync() 本身调用了另一个 await 方法,当遇到 await Task.Run() 行时,显然要等到 5 秒后才能完成,所以 DoWorkAsync() 不应该立即返回到调用方法 Main(),如果发生这种情况,Main() 是否应该像本书作者建议的那样继续打印“已完成”?
顺便说一句,这本书是针对 C# 7 的,但我正在使用 C# 8 运行 VS 2019,如果这有什么不同的话。
我强烈建议在 2012await
年引入关键字时阅读这篇博文,但它解释了异步代码在控制台程序中的工作原理:https : //devblogs.microsoft.com/pfxteam/await-synchronizationcontext-and-console-apps/
作者接着说
this 关键字 (await) 将始终修改返回 Task 对象的方法。当逻辑流到达
await
令牌时,调用线程将在此方法中挂起,直到调用完成。如果您要运行此版本的应用程序,您会发现“完成”消息显示在“完成工作!”之前。信息。如果这是一个图形应用程序,则用户可以在DoWorkAsync()
方法执行时继续使用 UI ”。
作者不严谨。
我会改变这个:
当逻辑流到达
await
token时,调用线程在这个方法中被挂起,直到调用完成
对此:
当逻辑的流动到达
await
令牌(这是后DoWorkAsync
返回一个Task
对象)时,函数的局部状态被保存在内存中的某个地方和正在运行的线程执行return
回异步调度程序(即线程池)。
我的观点是这await
不会导致线程“挂起”(也不会导致线程阻塞)。
下一句也有问题:
如果您要运行此版本的应用程序,您会发现“完成”消息显示在“完成工作!”之前。信息
(我假设“这个版本”作者指的是一个在语法上相同但省略了await
关键字的版本)。
提出的索赔是不正确的。被调用的方法DoWorkAsync
仍然返回一个Task<String>
无法有意义地传递给Console.WriteLine
:返回的Task<String>
必须是awaited
第一个。
查看有关 async/await 如何工作的各种在线文档和文章,我认为“await”会起作用,例如当遇到第一个“await”时,程序会检查该方法是否已经完成,如果没有,它会立即“ return”到调用方法,然后在等待任务完成后返回。
你的想法大体上是正确的。
但是如果调用方法是 Main() 本身,它返回给谁?它会简单地等待等待完成吗?这就是代码行为正常的原因(在打印“已完成”之前等待 5 秒)?
它返回到由 CLR 维护的默认线程池。每个 CLR 程序都有一个 Thread Pool,这就是为什么即使是最微不足道的 .NET 程序的进程也会出现在 Windows 任务管理器中,线程数在 4 到 10 之间。然而,这些线程中的大多数将被挂起(但是事实上,他们被暂停与async
/的使用无关await
。
但这引出了下一个问题:因为
DoWorkAsync()
这里本身调用了另一个await
ed 方法,当await Task.Run()
遇到该行时,显然要等到 5 秒后才能完成,不应DoWorkAsync()
立即返回调用方法Main()
,如果发生这种情况,不应Main()
按照书作者的建议继续打印“已完成”?
是与否:)
如果您查看已编译程序的原始 CIL (MSIL),它会有所帮助(这await
是一种纯粹的语法特性,不依赖于对 .NET CLR 的任何实质性更改,这就是为什么在.NET Framework 4.5 中引入了async
/await
关键字,甚至尽管 .NET Framework 4.5 运行在同一个 .NET 4.0 CLR 上,它比它早 3-4 年。
首先,我需要在语法上重新排列您的程序(此代码看起来不同,但它编译为与原始程序相同的 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!";
}
Run Code Online (Sandbox Code Playgroud)
这是发生的事情:
CLR 加载您的程序集并定位Main
入口点。
CLR 还使用它从操作系统请求的线程填充默认线程池,它会立即挂起这些线程(如果操作系统本身没有挂起它们 - 我忘记了这些细节)。
然后 CLR 选择一个线程作为主线程,另一个线程作为 GC 线程(对此有更多细节,我认为它甚至可能使用操作系统提供的主 CLR 入口点线程 - 我不确定这些细节)。我们将称之为Thread0
。
Thread0
然后Console.WriteLine(" Fun With Async ===>");
作为正常的方法调用运行。
Thread0
然后DoWorkAsync()
也作为普通的方法调用调用。
Thread0
(内部DoWorkAsync
)然后调用Task.Run
,将委托(函数指针)传递给BlockingJob
。
Task.Run
是“在线程池中的线程上调度(不是立即运行)此委托作为概念性“作业”的简写,并立即返回Task<T>
代表该作业状态的a 。
Task.Run
调用时耗尽或忙碌,则BlockingJob
在线程返回池之前根本不会运行 - 或者如果您手动增加池的大小。Thread0
然后立即给出一个Task<String>
代表 的生命周期和完成的BlockingJob
。请注意,此时该BlockingJob
方法可能尚未运行,也可能尚未运行,因为这完全取决于您的调度程序。
Thread0
然后遇到第一个await
forBlockingJob
的 Job Task<String>
。
DoWorkAsync
包含一个有效的return
语句,它导致实际执行返回到Main
,然后立即返回到线程池并让 .NET 异步调度程序开始担心调度。
因此,当Thread0
返回线程池时,BlockingJob
可能会或可能不会被调用,具体取决于您的计算机设置和环境(例如,如果您的计算机只有 1 个 CPU 内核,情况会有所不同 - 但还有许多其他事情!)。
Task.Run
把BlockingJob
作业放到调度,然后直到不可不实际运行它Thread0
自己返回线程池,然后调度运行BlockingJob
上Thread0
,整个程序只使用一个线程。Task.Run
会BlockingJob
立即在另一个池线程上运行(在这个简单的程序中很可能就是这种情况)。现在,假设Thread0
已经让步于池并Task.Run
在线程池 ( Thread1
) 中使用了不同的线程for BlockingJob
,那么Thread0
将被挂起,因为没有其他预定的延续(来自await
或ContinueWith
)也没有预定的线程池作业(来自Task.Run
或手动使用ThreadPool.QueueUserWorkItem
)。
Thread1
正在运行BlockingJob
并且它会休眠(阻塞)那 5 秒,因为Thread.Sleep
阻塞这就是为什么你应该总是喜欢Task.Delay
在async
代码中,因为它不会阻塞!)。Thread1
之后,然后解除阻塞并"Done with work!"
从该BlockingJob
调用返回- 并将该值返回到Task.Run
内部调度程序的调用站点,调度程序将BlockingJob
作业标记为完成,并"Done with work!"
作为结果值(这由Task<String>.Result
值表示)。Thread1
然后返回线程池。await
存在于先前在步骤 8 中使用的那个Task<String>
内部。DoWorkAsync
Thread0
Thread0
Task<String>
现在已经完成,它从线程池中挑选出另一个线程(可能是也可能不是Thread0
- 它可能是Thread1
或另一个不同的线程Thread2
- 同样,这取决于你的程序、你的计算机等 - 但最重要的是它取决于同步上下文以及您是否使用ConfigureAwait(true)
或ConfigureAwait(false)
)。
Thread2
。(我需要在此离题解释一下,虽然您的async Task<String> DoWorkAsync
方法是 C# 源代码中的单个方法,但在内部该DoWorkAsync
方法在每个await
语句中被拆分为“子方法” ,并且每个“子方法”都可以输入到直接地)。
struct
,用于捕获本地函数状态。参见脚注 2)。所以现在调度程序告诉Thread2
调用DoWorkAsync
对应于该逻辑的“子方法” await
。在这种情况下,它是String value = await threadTask;
线。
Task<String>.Result
is "Done with work!"
,因此它设置String value
为该字符串。然后调用的DoWorkAsync
子方法Thread2
也返回那个String value
- 但不是 to Main
,而是直接返回给调度程序 - 然后调度程序将该字符串值传递回Task<String>
for await messageTask
inMain
然后选择另一个线程(或同一个线程)到enter-intoMain
的子方法表示之后的代码await messageTask
,然后该线程Console.WriteLine( message );
以正常方式调用和其余代码。
请记住,挂起的线程与阻塞的线程不同:这是一种过度简化,但就本回答而言,“挂起的线程”具有空调用堆栈,调度程序可以立即将其投入工作以做一些有用的事情,而“阻塞的线程”有一个填充的调用堆栈,调度程序不能触摸它或重新调整它的用途,除非它返回到线程池 - 请注意,一个线程可以被“阻塞”,因为它很忙运行正常代码(例如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;
}
Run Code Online (Sandbox Code Playgroud)
编译器将生成在概念上与此 C# 对应的 CIL (MSIL)(即,如果它是在没有async
andawait
关键字的情况下编写的)。
(这段代码省略了很多细节,比如异常处理、 的真实值state
、内联AsyncTaskMethodBuilder
、 的捕获this
等等 - 但这些细节现在并不重要)
Task<String> FoobarAsync()
{
FoobarAsyncState state = new FoobarAsyncState();
state.state = 1;
state.task = new Task<String>();
state.MoveNext();
return state.task;
}
struct FoobarAsyncState
{
// Async state:
public Int32 state;
public Task<String> task;
// Locals:
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;
// This call below is a method in the `AsyncTaskMethodBuilder` which essentially instructs the scheduler to call this `FoobarAsyncState.MoveNext()` when `this.task1` completes.
// When `FoobarAsyncState.MoveNext()` is next called, the `case 2:` block will be executed because `this.state = 2` was assigned above.
AwaitUnsafeOnCompleted( this.task1.GetAwaiter(), this );
// Then immediately return to the caller (which will always be `FoobarAsync`).
return;
case 2:
this.value1 = this.task1.Result; // This doesn't block because `this.task1` will be completed.
this.task2 = GetDoubleAsync();
this.state = 3;
AwaitUnsafeOnCompleted( this.task2.GetAwaiter(), this );
// Then immediately return to the caller, which is most likely the thread-pool scheduler.
return;
case 3:
this.value2 = this.task2.Result; // This doesn't block because `this.task2` will be completed.
this.result = String.Format( "{0} {1}", value1, value2 );
// Set the .Result of this async method's Task<String>:
this.task.TrySetResult( this.result );
// `Task.TrySetResult` is an `internal` method that's actually called by `AsyncTaskMethodBuilder.SetResult`
// ...and it also causes any continuations on `this.task` to be executed as well...
// ...so this `return` statement below might not be called until a very long time after `TrySetResult` is called, depending on the contination chain for `this.task`!
return;
}
}
}
Run Code Online (Sandbox Code Playgroud)
请注意,出于性能原因,这FoobarAsyncState
是 astruct
而不是 aclass
我不会讨论。
归档时间: |
|
查看次数: |
419 次 |
最近记录: |