如何在.NET中实现和等待实现控制流?

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因为任务awaited已经完成(例如,它可以同步返回的情况)然后该方法继续通过状态,但是否则它将自己设置为对awaiter的回调.

发生什么事情取决于awaiter,就触发回调的内容而言(例如,异步I/O完成,在线程完成时运行的任务)以及对于特定线程的编组或在线程池线程上运行的要求,原始呼叫的上下文可能需要也可能不需要等等.不管它是什么,虽然awaiter中的东西会调用MoveNext它,它将继续下一个工作(直到下一个await)或完成并返回,在这种情况下Task它正在实现完成.

  • @DarioOO第一个我可以很快完成的工作,已经完成了从`yield`到手工翻译的大量翻译,这样做有一个好处(通常作为优化,但是要确保起点接近于编译器生成的,因此通过错误的假设没有任何东西变得非优化.第二个问题首先用于另一个答案中,当时我自己的知识存在一些空白,所以我从填充这些空白中获益,同时通过手工反编译代码来提供答案. (4认同)

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系列.


Chr*_*res 7

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是什么样的.它应该是有益的.

asyncawait另一方面,又是一整条鱼.摘要中,Await是一个同步原语.这是一种告诉系统的方法"在完成之前我不能继续".但是,正如你所指出的那样,并不总是涉及一个线程.

什么涉及的是一种叫做同步上下文.总有一个闲逛.同步上下文的工作是安排正在等待的任务及其继续.

当你说await thisThing(),有几件事情发生.在异步方法中,编译器实际上将方法切换为较小的块,每个块是"在await之前"部分和"在等待之后"(或继续)部分.当await执行时,等待的任务,以及后续的继续 - 换句话说,函数的其余部分 - 被传递给同步上下文.上下文负责调度任务,当它完成上下文然后运行continuation,传递它想要的任何返回值.

只要计划内容,同步上下文就可以随心所欲地执行任何操作.它可以使用线程池.它可以为每个任务创建一个线程.它可以同步运行它们.不同的环境(ASP.NET与WPF)提供了不同的同步上下文实现,这些实现根据对环境最有效的方式执行不同的操作.

(Bonus:曾经想知道.ConfigurateAwait(false)它有什么用?它告诉系统不要使用当前的同步上下文(通常基于你的项目类型 - 例如WPF vs ASP.NET),而是使用默认的一个,它使用线程池).

再说一遍,这是很多编译器的诡计.如果你看看生成的代码很复杂,但你应该能够看到它正在做什么.这些类型的转换很难,但确定性和数学,这就是编译器为我们做这些转换的原因.

PS默认同步上下文的存在有一个例外 - 控制台应用程序没有默认的同步上下文.查看Stephen Toub的博客以获取更多信息.这是寻找信息的好地方asyncawait一般.

  • 对不起,混淆了我的术语,我会修复帖子.基本上,不要对您所在的环境使用默认值,使用.NET的默认值(即线程池). (3认同)