在 .NET 中 yield 和 await 如何实现控制流? [英] How do yield and await implement flow of control in .NET?

查看:30
本文介绍了在 .NET 中 yield 和 await 如何实现控制流?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

据我所知 yield 关键字,如果在迭代器块内使用,它会将控制流返回给调用代码,当迭代器再次被调用时,它会从停止的地方开始.

As I understand the yield keyword, if used from inside an iterator block, it returns flow of control to the calling code, and when the iterator is called again, it picks up where it left off.

此外,await 不仅等待被调用者,还将控制权返回给调用者,仅在调用者 await 方法时从中断的地方继续.

Also, await not only waits for the callee, but it returns control to the caller, only to pick up where it left off when the caller awaits the method.

换句话说——没有线程,而async和await的并发"是巧妙的控制流造成的错觉,细节被语法隐藏了.

In other words-- there is no thread, and the "concurrency" of async and await is an illusion caused by clever flow of control, the details of which are concealed by the syntax.

现在,我是一名前汇编程序员,我非常熟悉指令指针、堆栈等,我了解正常的控制流(子例程、递归、循环、分支)是如何工作的.但是这些新的结构——我不明白.

Now, I'm a former assembly programmer and I'm very familiar with instruction pointers, stacks, etc. and I get how normal flows of control (subroutine, recursion, loops, branches) work. But these new constructs-- I don't get them.

当到达 await 时,运行时如何知道接下来应该执行哪一段代码?它如何知道何时可以从中断的地方恢复,以及它如何记住在哪里?当前调用堆栈会发生什么,是否以某种方式保存?如果调用方法在 awaits 之前调用其他方法会怎样——为什么堆栈没有被覆盖?在出现异常和堆栈展开的情况下,运行时到底如何完成所有这些工作?

When an await is reached, how does the runtime know what piece of code should execute next? How does it know when it can resume where it left off, and how does it remember where? What happens to the current call stack, does it get saved somehow? What if the calling method makes other method calls before it awaits-- why doesn't the stack get overwritten? And how on earth would the runtime work its way through all this in the case of an exception and a stack unwind?

当达到 yield 时,运行时如何跟踪应该拾取事物的点?如何保留迭代器状态?

When yield is reached, how does the runtime keep track of the point where things should be picked up? How is iterator state preserved?

推荐答案

我将在下面回答您的具体问题,但您可能只需阅读我关于我们如何设计 yield 和 await 的大量文章即可.

I'll answer your specific questions below, but you would likely do well to simply read my extensive articles on how we designed yield and await.

https://blogs.msdn.microsoft.com/ericlippert/tag/continuation-passing-style/

https://blogs.msdn.microsoft.com/ericlippert/tag/iterators/

https://blogs.msdn.microsoft.com/ericlippert/tag/async/

其中一些文章现在已经过时;生成的代码在很多方面都不同.但这些肯定会让您了解它的工作原理.

Some of these articles are out of date now; the code generated is different in a lot of ways. But these will certainly give you the idea of how it works.

此外,如果您不了解如何将 lambda 生成为闭包类,请先了解 .如果您没有关闭 lambda,您就不会出现异步的正面或反面.

Also, if you do not understand how lambdas are generated as closure classes, understand that first. You won't make heads or tails of async if you don't have lambdas down.

当到达等待时,运行时如何知道接下来应该执行哪一段代码?

When an await is reached, how does the runtime know what piece of code should execute next?

await 生成为:

if (the task is not completed)
  assign a delegate which executes the remainder of the method as the continuation of the task
  return to the caller
else
  execute the remainder of the method now

基本上就是这样.等待只是一个幻想的回报.

That's basically it. Await is just a fancy return.

它如何知道何时可以从上次中断的地方恢复,以及它如何记住在哪里?

How does it know when it can resume where it left off, and how does it remember where?

好吧,你如何在没有等待的情况下做到这一点?当方法 foo 调用方法 bar 时,不知何故我们记住了如何回到 foo 的中间,同时保持 foo 激活的所有局部变量,无论 bar 做什么.

Well, how do you do that without await? When method foo calls method bar, somehow we remember how to get back to the middle of foo, with all the locals of the activation of foo intact, no matter what bar does.

你知道这是如何在汇编程序中完成的.foo 的一个活动记录被压入堆栈;它包含当地人的价值观.在调用时,foo 中的返回地址被压入堆栈.当 bar 完成时,堆栈指针和指令指针被重置到它们需要的位置,而 foo 从它停止的地方继续前进.

You know how that's done in assembler. An activation record for foo is pushed onto the stack; it contains the values of the locals. At the point of the call the return address in foo is pushed onto the stack. When bar is done, the stack pointer and instruction pointer are reset to where they need to be and foo keeps going from where it left off.

await 的延续是完全一样的,除了记录被放到堆上的原因很明显,激活序列没有形成堆栈.

The continuation of an await is exactly the same, except that the record is put onto the heap for the obvious reason that the sequence of activations does not form a stack.

作为任务继续的等待给出的委托包含 (1) 一个数字,它是查找表的输入,它给出了您接下来需要执行的指令指针,以及 (2) locals 和临时工.

The delegate which await gives as the continuation to the task contains (1) a number which is the input to a lookup table that gives the instruction pointer that you need to execute next, and (2) all the values of locals and temporaries.

那里有一些额外的装备;例如,在 .NET 中,分支到 try 块的中间是非法的,因此您不能简单地将 try 块内的代码地址粘贴到表中.但这些都是簿记细节.从概念上讲,活动记录只是移动到堆上.

There is some additional gear in there; for instance, in .NET it is illegal to branch into the middle of a try block, so you can't simply stick the address of code inside a try block into the table. But these are bookkeeping details. Conceptually, the activation record is simply moved onto the heap.

当前调用堆栈会发生什么,是否以某种方式保存?

What happens to the current call stack, does it get saved somehow?

当前活动记录中的相关信息永远不会放在首位;它从一开始就从堆中分配.(好吧,形式参数通常在堆栈或寄存器中传递,然后在方法开始时复制到堆位置.)

The relevant information in the current activation record is never put on the stack in the first place; it is allocated off the heap from the get-go. (Well, formal parameters are passed on the stack or in registers normally and then copied into a heap location when the method begins.)

不存储调用者的激活记录;等待可能会返回给他们,记住,所以他们会被正常处理.

The activation records of the callers are not stored; the await is probably going to return to them, remember, so they'll be dealt with normally.

请注意,这是await的简化延续传递风格与您在Scheme等语言中看到的真正的call-with-current-continuation结构之间的密切区别.在这些语言中,包括继续返回调用者的整个延续被 call-cc 捕获.

Note that this is a germane difference between the simplified continuation passing style of await, and true call-with-current-continuation structures that you see in languages like Scheme. In those languages the entire continuation including the continuation back into the callers is captured by call-cc.

如果调用方法在等待之前调用其他方法会怎样——为什么堆栈没有被覆盖?

What if the calling method makes other method calls before it awaits-- why doesn't the stack get overwritten?

那些方法调用返回,因此它们的激活记录在等待点不再在堆栈上.

Those method calls return, and so their activation records are no longer on the stack at the point of the await.

在出现异常和堆栈展开的情况下,运行时究竟如何完成所有这些工作?

And how on earth would the runtime work its way through all this in the case of an exception and a stack unwind?

如果出现未捕获的异常,异常会被捕获,存储在任务中,并在获取任务结果时重新抛出.

In the event of an uncaught exception, the exception is caught, stored inside the task, and re-thrown when the task's result is fetched.

还记得我之前提到的所有簿记吗?让我告诉你,正确处理异常语义是一个巨大的痛苦.

Remember all that bookkeeping I mentioned before? Getting exception semantics right was a huge pain, let me tell you.

当达到 yield 时,运行时如何跟踪应该捡起东西的点?如何保留迭代器状态?

When yield is reached, how does the runtime keep track of the point where things should be picked up? How is iterator state preserved?

同样的方式.locals 的状态被移动到堆上,并且一个数字代表 MoveNext 下次被调用时应该恢复的指令与 locals 一起存储.

Same way. The state of locals is moved onto the heap, and a number representing the instruction at which MoveNext should resume the next time it is called is stored along with the locals.

再说一次,迭代器块中有一堆齿轮来确保正确处理异常.

And again, there's a bunch of gear in an iterator block to make sure that exceptions are handled correctly.

这篇关于在 .NET 中 yield 和 await 如何实现控制流?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆