当前位置: 首页 > 知识库问答 >
问题:

如何在中屈服并等待实现控制流。网络?

皇甫夕
2023-03-14

正如我所理解的产量关键字,如果从迭代器块内部使用,它会将控制流返回给调用代码,当再次调用迭代器时,它会从中断的地方继续。

而且,await不仅等待被调用者,而且还将控制权返回给调用者,只在调用者等待方法时选择停止的位置。

换句话说,没有线程,async和await的“并发性”是由巧妙的控制流引起的幻觉,其细节被语法所掩盖。

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

当到达等待时,运行时如何知道下一步应该执行哪段代码?它如何知道何时可以恢复到中断的位置,又如何记得在哪里?当前调用堆栈会发生什么情况,是否会以某种方式保存?如果调用方法在等待之前进行其他方法调用,会怎么样?为什么堆栈没有被覆盖?在出现异常和堆栈展开的情况下,运行时究竟将如何处理所有这些问题?

当达到产量时,运行时如何跟踪应该拾取东西的点?迭代器状态如何保存?

共有3个答案

萧自珍
2023-03-14

这里已经有很多伟大的答案;我只想分享一些有助于形成心智模型的观点。

首先,编译器将异步方法分解为若干部分;等待表达式是破裂点。(对于简单的方法来说,这很容易想到;带有循环和异常处理的更复杂的方法也会被分解,添加了更复杂的状态机)。

其次,wait被翻译成一个相当简单的序列;我喜欢Lucian的描述,换句话说就是“如果可等待的已经完成,则获取结果并继续执行此方法;否则,保存此方法的状态并返回”。(我在我的async介绍中使用了非常相似的术语)。

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

该方法的其余部分作为可等待的回调存在(在任务的情况下,这些回调是延续)。当可等待完成时,它会调用其回调。

请注意,调用堆栈没有保存和恢复;回调是直接调用的。在I/O重叠的情况下,它们直接从线程池调用。

这些回调可以直接继续执行该方法,也可以将其安排在其他地方运行(例如,如果等待捕获了UI同步上下文,并且线程池上的I/O已完成)。

它如何知道何时可以恢复到中断的位置,又如何记得在哪里?

这些都是回电。当等待完成时,它调用其回调,任何已经等待的异步方法都会恢复。回调跳到该方法的中间,并在范围内包含其局部变量。

回调不会运行特定的线程,也不会恢复其调用堆栈。

当前调用堆栈会发生什么情况,是否会以某种方式保存?如果调用方法在等待之前进行了其他方法调用,会怎么样?为什么堆栈没有被覆盖?在出现异常和堆栈展开的情况下,运行时究竟将如何处理所有这些问题?

首先不保存调用堆栈;这是没有必要的。

使用同步代码,您可以得到一个包含所有调用者的调用堆栈,并且运行时知道使用它返回到哪里。

使用异步代码,您可以得到一组回调指针,这些指针以完成其任务的某个I/O操作为根,可以恢复完成其任务的异步方法,也可以恢复完成其任务的异步方法,等等。

因此,使用同步代码调用A调用B调用C,调用堆栈可能如下所示:

A:B:C

而异步代码使用回调(指针):

A <- B <- C <- (I/O operation)

当达到收益率时,运行时如何跟踪应该提取内容的点?迭代器状态是如何保持的?

目前,效率相当低。:)

它的工作原理与其他lambda一样,变量的生命周期被延长,引用被放置到堆栈上的状态对象中。所有深层次细节的最佳资源是Jon Skeet的EduAsync系列。

宰父夕
2023-03-14

产量是两者中更容易的,所以让我们来检查一下。

假设我们有:

public IEnumerable<int> CountToTen()
{
  for (int i = 1; i <= 10; ++i)
  {
    yield return i;
  }
}

这就像我们编写的:

// 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();
    }
}

因此,不如IEnumable的手写实现高效

当然从那以后

foreach(var a in b)
{
  DoSomething(a);
}

与以下内容相同:

using(var en = b.GetEnumerator())
{
  while(en.MoveNext())
  {
     var a = en.Current;
     DoSomething(a);
  }
}

然后,重复调用生成的MoveNext()

async案例几乎是相同的原理,但有一点额外的复杂性。要重用另一个答案代码中的示例,例如:

private async Task LoopAsync()
{
    int count = 0;
    while(count < 5)
    {
       await SomeNetworkCallAsync();
       count++;
    }
}

生成如下代码:

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;
}

这更复杂,但基本原理非常相似。主要的额外复杂性是现在正在使用的是GetAwaiter()。如果有时间等待。如果选中IsCompleted(已完成),则返回true(真),因为任务Wait(等待)已经完成(例如,它可以同步返回的情况),然后该方法继续在状态中移动,但在其他情况下,它将自身设置为对等待者的回调。

这会发生什么取决于等待者,取决于触发回调的因素(例如异步I/O完成、在线程上运行的任务完成)、编组到特定线程或在线程池线程上运行的要求、可能需要或不需要原始调用的上下文等。不管是什么,等待者中的某些内容都会调用MoveNext,它要么继续下一项工作(直到下一项工作),要么完成并返回,在这种情况下,它正在执行的任务就完成了。

景胜涝
2023-03-14

我将在下面回答您的具体问题,但您最好简单地阅读我关于如何设计产量和等待的大量文章。

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/

这些文章中有些已经过时了;生成的代码在很多方面都不同。但这些肯定会让你了解它是如何工作的。

此外,如果您不了解lambda是如何作为闭包类生成的,请首先了解这一点。如果不关闭lambdas,您就不会了解异步。

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

等待生成为:

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

基本上就是这样。等待只是一种花哨的回报。

它如何知道何时可以恢复到中断的位置,又如何记得在哪里?

那么,您如何在没有等待的情况下做到这一点?当方法foo调用方法bar时,不知何故,我们记得如何回到foo的中间,并且所有激活foo的本地人都完好无损,无论bar做什么。

你知道在汇编程序中是怎么做的。foo的激活记录被压入堆栈;它包含本地值。在调用时,foo中的返回地址被压入堆栈。当bar完成时,堆栈指针和指令指针被重置到它们需要的位置,foo继续从它停止的地方前进。

等待的延续是完全相同的,只是记录被放在堆上,原因很明显,激活序列没有形成堆栈。

等待的委托作为任务的延续,包含(1)一个数字,该数字是查找表的输入,该表给出了下一步需要执行的指令指针,以及(2)局部变量和临时变量的所有值。

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

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

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

未存储呼叫者的激活记录;记住,等待很可能会回到他们身上,所以他们会得到正常的处理。

请注意,这是wait的简化延续传递样式与您在Schem等语言中看到的真正的带有当前延续的调用结构之间的密切区别。在这些语言中,包括返回调用者的延续在内的整个延续都由call-cc捕获。

如果调用方法在等待之前进行了其他方法调用怎么办——为什么堆栈不被覆盖?

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

在出现异常和堆栈展开的情况下,运行时究竟将如何处理所有这些问题?

如果发生未捕获的异常,将捕获该异常,并将其存储在任务中,然后在获取任务结果时重新抛出。

还记得我之前提到的簿记吗?让我告诉你,正确掌握例外语义学是一个巨大的痛苦。

当达到收益率时,运行时如何跟踪应该提取内容的点?迭代器状态是如何保持的?

同样的方式。本地人的状态被移动到堆上,一个代表MoveNext下次调用时应在其上恢复的指令的数字与本地人一起存储。

同样,迭代器块中有一组齿轮,以确保正确处理异常。

 类似资料:
  • 问题内容: 我们正在将Selenium与Java API和一些Javascript用户扩展一起使用。我们在应用程序中使用了很多AJAX调用。我们的许多测试都是随机失败的,因为有时AJAX调用的完成速度比其他时间慢,因此页面未完全加载。我们通过等待特定元素或Thread.sleep来解决此问题。我试图找到一种方法来代替等待网络流量完成。这样我们就可以做到这一点: 这样,我们可以摆脱线程hiberna

  • 问题内容: 我们正在将Selenium与Java API和一些Javascript用户扩展一起使用。我们在应用程序中使用了很多AJAX调用。我们的许多测试都是随机失败的,因为有时AJAX调用的完成速度比其他时间慢,因此页面未完全加载。我们通过等待特定元素或Thread.sleep来解决此问题。我试图找到一种方法来代替等待网络流量完成。这样我们就可以做到这一点: 这样,我们可以摆脱线程休眠,并在服务

  • 问题内容: 示例了如何使用内置的加密库和流来计算文件的md5。 但是是否可以将其转换为使用ES8异步/等待而不是使用上述回调,但仍保持使用流的效率? 问题答案: / 仅适用于promise,不适用于流。有一些想法可以制作一种类似流的额外数据类型,该数据类型将具有自己的语法,但是如果有的话,这些想法是高度实验性的,我将不赘述。 无论如何,您的回调仅等待流结束,这非常适合兑现承诺。您只需要包装流: 现

  • 本文向大家介绍selenium WebDriverWait类等待机制的实现,包括了selenium WebDriverWait类等待机制的实现的使用技巧和注意事项,需要的朋友参考一下 在自动化测试脚本的运行过程中,可以通过设置等待的方式来避免由于网络延迟或浏览器卡顿导致的偶然失败,常用的等待方式有三种: 一、固定等待(time)   固定待是利用python语言自带的time库中的sleep()方

  • 这个网站我正在使用jSoup解析加载增量。我试图访问的数据在几秒钟后加载到页面中,但jSoup只获取最初加载的所有内容。 有没有办法强制jsoup在尝试解析页面之前等待页面完全加载,或者在延迟中构建页面以允许页面完全加载?

  • 问题内容: java中如何实现访问控制,public, protected, default和private关键字有何区别? 问题答案: