【问题标题】:Confused at control flow of async/await of c#对 c# 的 async/await 的控制流感到困惑
【发布时间】:2018-12-10 13:18:20
【问题描述】:

我正在学习 async/await ,对 MSDN 的 await 解释感到困惑:"await 运算符暂停执行,直到 GetByteArrayAsync 方法的工作完成。在同时,控制权返回给 GetPageSizeAsync 的调用者"

我不明白的是,“返回”是什么意思?起初,我相信当一个线程(比如说UI线程)到达“await”关键字时,系统会创建一个新线程(或从threadPool中获取一个线程)来执行剩下的代码,UI线程可以返回调用者方法并执行其余部分。

但现在我知道“等待”永远不会创建线程。

我写了一个演示:

class Program
{
    static void Main(string[] args)
    {
        new Test().M1();
        Console.WriteLine("STEP:8");
        Console.Read();
    }
}

class Test
{
    public async void M1()
    {
        Console.WriteLine("STEP:1");
        var t = M2();
        Console.WriteLine("STEP:3");
        await t;
    }
    public async Task<string> M2()
    {
        Console.WriteLine("STEP:2");
        string rs = await M3();//when the thread reaches here,why don't it return to M1 and execute the STEP:3 ??
        Console.WriteLine("STEP:7");
        return rs;

    }

    public async Task<string> M3()
    {
        Console.WriteLine("STEP:4");
        var rs = Task.Run<string>(() => {
            Thread.Sleep(3000);//simulate some work that takes 3 seconds
            Console.WriteLine("STEP:6");
            return "foo";
        });
        Console.WriteLine("STEP:5");
        return await rs;
    }
}

这个演示打印了一些代表执行流程的标签,我认为它会是

步骤:1

步骤:2

步骤:3

步骤:4

步骤:5

步骤:6

步骤:7

步骤:8

(从 1 到 8 的顺序),

但实际结果是: Figure 1

步骤:1

步骤:2

步骤:4

步骤:5

步骤:3

步骤:8

步骤:6

步骤:7

如果像 MSDN 的解释一样,控制权返回到 M1 并打印“STEP 3”,我绝对错了,那么到底发生了什么?

提前致谢

【问题讨论】:

  • 要清楚 - async "just" 改变了您可以在方法中写入的内容。 await 不会开始任何事情发生。你总是向await 传递一些代表已经开始的工作的东西。开始工作的方式和原因不是await 关心的问题。
  • 我已经发布了相当长的答案,而且相当罗嗦。但是,我觉得有必要解决您的问题和困惑。如果您发现它有问题,请告诉我。我可能很快就会回来并添加进一步阅读的链接。

标签: c# async-await


【解决方案1】:

只是为了解决这个问题,不能保证在任何特定的await 点上,除了继续处理其余代码之外,我们会做任何事情。但是,在 MSDN 中的示例和您自己的示例中,我们将始终在每个 await 点处等待。

所以,我们达到了await z 的某个点,我们决定等待。这意味着 a) z 还没有完成(无论 z 完成意味着什么不是我们现在关心的事情)和 b) 我们自己没有有用的工作要做,目前。

希望通过以上内容,您可以明白为什么“创建新线程”或类似的东西没有必要,因为就像我刚才所说的,没有什么有用的工作要做。

不过,在我们从方法返回控制权之前,我们要将一个延续加入队列。 async 机器能够表达“ z 完成时,安排 方法的其余部分从我们的await 点继续”。

在这里,同步上下文和ConfigureAwait 变得相关。我们正在运行的线程(以及我们即将放弃控制的线程)在某些方面可能是“特殊的”。它可能是 UI 线程。它目前可能拥有对某些资源的独占访问权(想想 ASP.Net pre-Core Request/Response/Session 对象)。

如果是这样,希望提供特殊性的系统已经安装了一个同步上下文,并且通过它我们也能够获得“我们恢复执行这个方法时,我们需要有与我们以前相同的特殊情况”。所以例如我们能够在 UI 线程上继续运行我们的方法

默认情况下,没有同步上下文,会找到一个线程池线程来运行我们的延续。

请注意,如果一个方法包含多个需要等待的awaits,那么在第一次等待之后,我们将作为链式延续运行。我们的原始调用者在我们awaited 时第一次获取了他们的上下文。


在您的示例中,我们有三个 await 点,所有三个点等待并导致我们将控制权交还给调用者(我们没有多个 awaits用一种方法来担心)。归根结底,所有这些等待都在等待Thread.Sleep 完成(现代async 代码中的不良形式应该改用TaskDelay)。因此,任何出现在await 之后的Console.WriteLines 在该方法中都会被延迟。

但是,M1async void,最好避免在事件处理程序之外使用。 async void 方法存在问题,因为它们无法确定何时完成。

Main 调用M1 打印1 然后调用M2M2 打印 2 然后调用 M3M3 打印4,创建一个新的Task,打印5,然后 才能放弃控制权。正是在这一点上,它创建了一个 TaskCompletionSource 来表示它的最终完成,从中获取 Task 并返回它。

请注意,只有当对M3 的调用返回时,我们才会点击M2 中的await。我们也必须在这里等待,所以我们做的几乎与M3 相同,并返回Task

现在M1 终于有了一个Task,它可以在await 上运行。但在此之前,它会打印出3。它将控制权返回给Main,现在打印8

过了很久,rsM3 中的 Thread.Sleep 完成运行,我们打印 6M3 将其返回的 Task 标记为完成,没有更多工作要做,因此退出,没有安排进一步的继续。

M2 现在可以恢复其await 并打印6。完成后,它的Task 就完成了,M1 终于可以恢复并打印7

M1 没有 Task 标记为完成,因为它是 async void

【讨论】:

  • 很好的解释。
  • @Oliver - 这是我试图为这个问题写的第五个答案中的第四个,即便如此,在我发布它之前它也进行了认真的修改。这里有很多微妙之处值得探索。
  • 但是这些字幕大部分与手头的代码无关,问题问到了。无需编写完整的 await 教程来回答所提出的问题。如果这就是读者想要的,那么很多教程已经存在了。
  • @Servy - 好吧,他们似乎关心 UI 线程,多次提到它,这就是我想讨论同步上下文的原因,例如。
  • @ Damien_The_Unbeliever 多么好的解释!你知道吗,当我读到“请注意,只有当对 M3 的调用返回时,我们才会在 M2 中等待等待。我们也必须在这里等待,所以我们做的几乎与 M3 相同并返回一个任务”,我突然明白了一切. “await”正在等待一些有意义的工作(这是一个任务),所以M3将打印4&5然后返回一个任务,M2中的“等待”现在得到一个等待的任务,同时它返回到M1,打印3,然后在M1中再次“等待”返回Main打印8。3秒后M3中的任务打印6并自行完成,因此M2可以继续,打印7。
【解决方案2】:

当您查看这段代码在后台实际执行的操作时,最容易看出这一点。以下是该代码正在执行的操作的大致翻译:

public void M1()
{
    Console.WriteLine("STEP:1");
    var t = M2();
    Console.WriteLine("STEP:3");
    t.ContinueWith(_ =>
    {
        //do nothing
    });
}
public Task<string> M2()
{
    Console.WriteLine("STEP:2");
    Task<string> task = M3();
    return task.ContinueWith(t =>
    {
        Console.WriteLine("STEP:7");
        return t.Result;
    });
}
public Task<string> M3()
{
    Console.WriteLine("STEP:4");
    var rs = Task.Run<string>(() =>
    {
        Thread.Sleep(3000);//simulate some work that takes 3 seconds
        Console.WriteLine("STEP:6");
        return "foo";
    });
    Console.WriteLine("STEP:5");
    return rs.ContinueWith(t => t.Result);
}

请注意,为简洁起见,我没有在此处进行适当的错误处理。使用await 的最大好处之一是它以您通常希望它们的方式处理错误,并且使用我在这里拥有的工具......没有。此代码也没有安排延续使用当前同步上下文,这与这种情况无关,但它是一个有用的功能。

在这一点上,希望更清楚发生了什么。对于初学者来说,M1M3 首先确实没有必要成为 async,因为他们在后续工作中从未做任何有意义的事情。他们真的不应该是async 并返回他们通过调用其他方法获得的任务。 M2 是唯一在它创建的任何延续中实际执行任何有用的方法。

它还使操作顺序更加清晰。我们调用M1,它调用M2,它调用M3,它调用Task.Run,然后M3返回一个与Task.Run返回的相同的任务,然后返回自己,然后M2添加它的延续并返回,然后M3 执行下一个打印行,然后添加它的延续(它最终运行时什么都不做),然后返回。然后在稍后的某个时间点,Task.Run 完成,所有的延续都自下而上地运行(因为每个都是另一个的延续)。

【讨论】:

  • 谢谢@Servy,现在我想我应该在 TPL 中挖掘它以查看详细信息。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-09-24
  • 1970-01-01
  • 1970-01-01
  • 2019-01-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多