Joh*_* Wu 103 .net c# asynchronous async-await
据我所知,yield
关键字,如果从迭代器块内部使用,它会将控制流返回给调用代码,当再次调用迭代器时,它会从中断处继续.
此外,await
不仅等待被调用者,而且还将控制权返回给调用者,仅在调用者调用awaits
方法时从中断处获取.
换句话说 - 没有线程,async和await的"并发"是一种由巧妙的控制流引起的错觉,其细节被语法隐藏.
现在,我是一名前汇编程序员,我对指令指针,堆栈等非常熟悉,并且我得到了正常的控制流(子程序,递归,循环,分支)的工作方式.但是这些新的结构 - 我没有得到它们.
当await
到达,如何运行时知道什么是一段代码接下来应该执行?它是如何知道什么时候可以从它停止的地方恢复的,它如何记住在哪里?当前的调用堆栈会发生什么,它会以某种方式保存吗?如果调用方法在其之前进行其他方法调用,那么该await
怎么办?为什么堆栈不会被覆盖呢?在异常和堆栈展开的情况下,运行时如何在所有这些中运行?
何时yield
到达,运行时如何跟踪应该拾取事物的点?迭代器状态如何保存?
Eri*_*ert 110
我将在下面回答您的具体问题,但您可能只需阅读我关于我们如何设计产量和等待的大量文章.
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/
其中一些文章现已过时; 生成的代码在很多方面都有所不同.但这些肯定会让你知道它是如何工作的.
此外,如果您不理解lambdas如何作为闭包类生成,请首先理解.如果没有lambdas,你不会做出异步的正面或反面.
当达到await时,运行时如何知道接下来应该执行哪一段代码?
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
Run Code Online (Sandbox Code Playgroud)
基本上就是这样.等待只是一个奇特的回报.
它是如何知道什么时候可以从它停止的地方恢复的,它如何记住在哪里?
那么,你如何做到这一点没有等待?当方法foo调用方法栏时,不知怎的,我们记得如何回到foo的中间,foo的激活的所有本地都完好无损,不管是什么吧.
你知道在汇编程序中是如何完成的.foo的激活记录被压入堆栈; 它包含本地人的价值观.在调用时,foo中的返回地址被压入堆栈.当bar完成时,堆栈指针和指令指针被重置到它们需要的位置,并且foo从它停止的位置继续前进.
await的延续完全相同,只是记录被放到堆上,原因很明显,激活序列不会形成堆栈.
等待作为任务的继续提供的委托包含(1)一个数字,它是查找表的输入,它给出了接下来需要执行的指令指针,以及(2)本地和临时值的所有值.
那里还有一些额外的装备; 例如,在.NET中,分支到try块的中间是非法的,因此您不能简单地将try块中的代码地址粘贴到表中.但这些都是簿记细节.从概念上讲,激活记录只是移动到堆上.
当前的调用堆栈会发生什么,它会以某种方式保存吗?
当前激活记录中的相关信息从不首先放在堆栈中; 它从一开始就从堆中分配出来.(好的,正式参数通常在堆栈或寄存器中传递,然后在方法开始时复制到堆位置.)
不存储呼叫者的激活记录; 等待可能会回归他们,记住,所以他们将被正常处理.
请注意,这是await的简化延续传递样式与您在Scheme等语言中看到的真实的call-with-current-continuation结构之间的密切差异.在这些语言中,call-cc捕获了包括继续回调用者的整个延续.
如果调用方法在等待之前进行其他方法调用会怎样 - 为什么堆栈不会被覆盖?
那些方法调用返回,因此它们的激活记录不再位于等待点的堆栈上.
在异常和堆栈展开的情况下,运行时如何在所有这些中运行?
如果发生未捕获的异常,则会捕获异常,将其存储在任务中,并在获取任务结果时重新抛出异常.
还记得我之前提到的所有簿记吗?正确地获取异常语义是一个巨大的痛苦,让我告诉你.
达到产量时,运行时如何跟踪应该拾取的点?迭代器状态如何保存?
同样的方式.将本地的状态移动到堆上,并且表示将在MoveNext
下次调用时恢复的指令的数字与本地一起存储.
同样,迭代器块中有一堆齿轮,以确保正确处理异常.
Jon*_*nna 37
yield
两者中哪一个更容易,所以让我们来研究一下.
说我们有:
public IEnumerable<int> CountToTen()
{
for (int i = 1; i <= 10; ++i)
{
yield return i;
}
}
Run Code Online (Sandbox Code Playgroud)
编译有点像我们写的:
// Deliberately use name that isn't valid C# to not clash with anything
private class <CountToTen> : IEnumerator<int>, IEnumerable<int>
{
private int _i;
private int _current;
private int _state;
private int _initialThreadId = CurrentManagedThreadId;
public IEnumerator<CountToTen> GetEnumerator()
{
// Use self if never ran and same thread (so safe)
// otherwise create a new object.
if (_state != 0 || _initialThreadId != CurrentManagedThreadId)
{
return new <CountToTen>();
}
_state = 1;
return this;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public int Current => _current;
object IEnumerator.Current => Current;
public bool MoveNext()
{
switch(_state)
{
case 1:
_i = 1;
_current = i;
_state = 2;
return true;
case 2:
++_i;
if (_i <= 10)
{
_current = _i;
return true;
}
break;
}
_state = -1;
return false;
}
public void Dispose()
{
// if the yield-using method had a `using` it would
// be translated into something happening here.
}
public void Reset()
{
throw new NotSupportedException();
}
}
Run Code Online (Sandbox Code Playgroud)
所以,还不如一个手写的执行效率IEnumerable<int>
和IEnumerator<int>
(例如,我们可能不会有一个单独的浪费_state
,_i
而且_current
在这种情况下),但不坏(的伎俩再利用自身安全情况下这样做,而不是创建一个新的对象很好),可扩展以处理非常复杂的yield
使用方法.
当然,从那以后
foreach(var a in b)
{
DoSomething(a);
}
Run Code Online (Sandbox Code Playgroud)
是相同的:
using(var en = b.GetEnumerator())
{
while(en.MoveNext())
{
var a = en.Current;
DoSomething(a);
}
}
Run Code Online (Sandbox Code Playgroud)
然后MoveNext()
重复调用生成的.
的async
情况下,几乎是同样的原则,但有一点额外的复杂性.要重用另一个答案代码中的示例,例如:
private async Task LoopAsync()
{
int count = 0;
while(count < 5)
{
await SomeNetworkCallAsync();
count++;
}
}
Run Code Online (Sandbox Code Playgroud)
生成如下代码:
private struct LoopAsyncStateMachine : IAsyncStateMachine
{
public int _state;
public AsyncTaskMethodBuilder _builder;
public TestAsync _this;
public int _count;
private TaskAwaiter _awaiter;
void IAsyncStateMachine.MoveNext()
{
try
{
if (_state != 0)
{
_count = 0;
goto afterSetup;
}
TaskAwaiter awaiter = _awaiter;
_awaiter = default(TaskAwaiter);
_state = -1;
loopBack:
awaiter.GetResult();
awaiter = default(TaskAwaiter);
_count++;
afterSetup:
if (_count < 5)
{
awaiter = _this.SomeNetworkCallAsync().GetAwaiter();
if (!awaiter.IsCompleted)
{
_state = 0;
_awaiter = awaiter;
_builder.AwaitUnsafeOnCompleted<TaskAwaiter, TestAsync.LoopAsyncStateMachine>(ref awaiter, ref this);
return;
}
goto loopBack;
}
_state = -2;
_builder.SetResult();
}
catch (Exception exception)
{
_state = -2;
_builder.SetException(exception);
return;
}
}
[DebuggerHidden]
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine param0)
{
_builder.SetStateMachine(param0);
}
}
public Task LoopAsync()
{
LoopAsyncStateMachine stateMachine = new LoopAsyncStateMachine();
stateMachine._this = this;
AsyncTaskMethodBuilder builder = AsyncTaskMethodBuilder.Create();
stateMachine._builder = builder;
stateMachine._state = -1;
builder.Start(ref stateMachine);
return builder.Task;
}
Run Code Online (Sandbox Code Playgroud)
它更复杂,但却是一个非常相似的基本原理.主要的额外复杂因素是现在GetAwaiter()
正在使用.如果awaiter.IsCompleted
检查任何时间,则返回true
因为任务await
ed已经完成(例如,它可以同步返回的情况)然后该方法继续通过状态,但是否则它将自己设置为对awaiter的回调.
发生什么事情取决于awaiter,就触发回调的内容而言(例如,异步I/O完成,在线程完成时运行的任务)以及对于特定线程的编组或在线程池线程上运行的要求,原始呼叫的上下文可能需要也可能不需要等等.不管它是什么,虽然awaiter中的东西会调用MoveNext
它,它将继续下一个工作(直到下一个await
)或完成并返回,在这种情况下Task
它正在实现完成.
Ste*_*ary 13
这里有很多很棒的答案; 我将分享一些可以帮助形成心理模型的观点.
首先,async
编译器将方法分成几部分; 该await
表达式是断裂点.(对于简单的方法,这很容易设想;带有循环和异常处理的更复杂的方法也会被分解,添加更复杂的状态机).
第二,await
被翻译成一个相当简单的序列; 我喜欢Lucian的描述,在语言中几乎是"如果等待已经完成,得到结果并继续执行此方法;否则,保存此方法的状态并返回".(我在我的async
介绍中使用了非常相似的术语).
当达到await时,运行时如何知道接下来应该执行哪一段代码?
该方法的其余部分作为可回收的回调存在(在任务的情况下,这些回调是连续的).当等待完成时,它会调用它的回调.
请注意,不会保存和恢复调用堆栈; 回调是直接调用的.在重叠I/O的情况下,它们直接从线程池调用.
那些回调可以继续直接执行该方法,或者它们可以将其安排在其他地方运行(例如,如果await
捕获的UI SynchronizationContext
和I/O在线程池上完成).
它是如何知道什么时候可以从它停止的地方恢复的,它如何记住在哪里?
这都是回调.当awaable完成时,它会调用它的回调,并且async
已经await
编辑它的任何方法都会恢复.回调跳转到该方法的中间,并在其范围内包含其局部变量.
回调不会运行特定的线程,并且它们没有恢复其callstack.
当前的调用堆栈会发生什么,它会以某种方式保存吗?如果调用方法在等待之前进行其他方法调用会怎样 - 为什么堆栈不会被覆盖?在异常和堆栈展开的情况下,运行时如何在所有这些中运行?
首先没有保存callstack; 没有必要.
使用同步代码,您最终可以得到一个包含所有调用者的调用堆栈,并且运行时知道使用该调用返回的位置.
使用异步代码,您最终可以获得一堆回调指针 - 根据完成其任务的某些I/O操作,可以恢复async
完成其任务的async
方法,该方法可以恢复完成其任务的方法等.
因此,使用同步代码A
调用B
调用时C
,您的callstack可能如下所示:
A:B:C
Run Code Online (Sandbox Code Playgroud)
而异步代码使用回调(指针):
A <- B <- C <- (I/O operation)
Run Code Online (Sandbox Code Playgroud)
达到产量时,运行时如何跟踪应该拾取的点?迭代器状态如何保存?
目前,相当低效.:)
它的工作方式与任何其他lambda一样 - 扩展了变量生命周期,并将引用放入堆栈中的状态对象.所有深层细节的最佳资源是Jon Skeet的EduAsync系列.
yield
并且await
,虽然都处理流量控制,但两个完全不同的东西.所以我会分开处理它们.
目标yield
是使构建延迟序列更容易.当你编写一个带有yield
语句的枚举器循环时,编译器会生成大量你看不到的新代码.在引擎盖下,它实际上产生了一个全新的类.该类包含跟踪循环状态的成员,以及IEnumerable的实现,以便每次MoveNext
通过该循环再次调用它时.所以当你像这样做一个foreach循环时:
foreach(var item in mything.items()) {
dosomething(item);
}
Run Code Online (Sandbox Code Playgroud)
生成的代码看起来像:
var i = mything.items();
while(i.MoveNext()) {
dosomething(i.Current);
}
Run Code Online (Sandbox Code Playgroud)
在mything.items()的实现中,有一堆状态机代码,它们将循环执行一个"步骤"然后返回.所以当你在源代码中编写它就像一个简单的循环时,它不是一个简单的循环.所以编译技巧.如果你想看到自己,请拉出ILDASM或ILSpy或类似的工具,看看生成的IL是什么样的.它应该是有益的.
async
而await
另一方面,又是一整条鱼.摘要中,Await是一个同步原语.这是一种告诉系统的方法"在完成之前我不能继续".但是,正如你所指出的那样,并不总是涉及一个线程.
什么是涉及的是一种叫做同步上下文.总有一个闲逛.同步上下文的工作是安排正在等待的任务及其继续.
当你说await thisThing()
,有几件事情发生.在异步方法中,编译器实际上将方法切换为较小的块,每个块是"在await之前"部分和"在等待之后"(或继续)部分.当await执行时,等待的任务,以及后续的继续 - 换句话说,函数的其余部分 - 被传递给同步上下文.上下文负责调度任务,当它完成上下文然后运行continuation,传递它想要的任何返回值.
只要计划内容,同步上下文就可以随心所欲地执行任何操作.它可以使用线程池.它可以为每个任务创建一个线程.它可以同步运行它们.不同的环境(ASP.NET与WPF)提供了不同的同步上下文实现,这些实现根据对环境最有效的方式执行不同的操作.
(Bonus:曾经想知道.ConfigurateAwait(false)
它有什么用?它告诉系统不要使用当前的同步上下文(通常基于你的项目类型 - 例如WPF vs ASP.NET),而是使用默认的一个,它使用线程池).
再说一遍,这是很多编译器的诡计.如果你看看生成的代码很复杂,但你应该能够看到它正在做什么.这些类型的转换很难,但确定性和数学,这就是编译器为我们做这些转换的原因.
PS默认同步上下文的存在有一个例外 - 控制台应用程序没有默认的同步上下文.查看Stephen Toub的博客以获取更多信息.这是寻找信息的好地方async
和await
一般.
归档时间: |
|
查看次数: |
6897 次 |
最近记录: |