简体   繁体   English

在c#的异步/等待的控制流中感到困惑

[英]Confused at control flow of async/await of c#

I'm learning about async/await ,and get confused on the explaination of the await of MSDN :" The await operator suspends execution until the work of the GetByteArrayAsync method is complete. In the meantime, control is returned to the caller of GetPageSizeAsync " 我正在学习async / await,并对MSDN的await的解释感到困惑:“ await运算符暂停执行,直到GetByteArrayAsync方法的工作完成。同时,控制权返回给GetPageSizeAsync的调用者

What i don't understand is,what does it mean "returned"? 我不明白的是,“返回”是什么意思? At first,i believed when a thread(let's say, the UI thread) reaches the "await" keyword, system will create a new thread(or get a thread from threadPool) to execute the rest of codes,and UI thread can return to the caller method and execute the rest. 起初,我相信当一个线程(比方说,UI线程)到达“await”关键字时,系统将创建一个新线程(或从threadPool获取一个线程)来执行其余的代码,并且UI线程可以返回调用方法并执行其余的操作。

But now i know that "await" will never create thread. 但现在我知道“等待”永远不会创造线程。

I write a demo: 我写了一个演示:

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

this demo prints some labels representing the execute flow,which i thought it would be 这个演示打印一些代表执行流程的标签,我认为它会是

STEP:1 第1步

STEP:2 第2步

STEP:3 步骤:3

STEP:4 第四步

STEP:5 STEP:5

STEP:6 STEP:6

STEP:7 STEP:7

STEP:8 STEP:8

(from 1 to 8 in order), (按顺序从1到8),

but the actual result is : Figure 1 但实际结果如下: 图1

STEP:1 第1步

STEP:2 第2步

STEP:4 第四步

STEP:5 STEP:5

STEP:3 步骤:3

STEP:8 STEP:8

STEP:6 STEP:6

STEP:7 STEP:7

If like MSDN's explaination, control is returned to M1 and print the "STEP 3", absolutely i'm wrong,So what's going on exactly? 如果像MSDN的解释一样,控制权返回M1并打印“STEP 3”,绝对是错的,那究竟发生了什么?

Thanks in advance 提前致谢

Just to get it out of the way, there's no guarantee that, at any particular await point we're going to do anything beyond carrying on with the rest of the code. 只是为了让它不受影响,我们无法保证,在任何特定的await点,除了继续执行其余的代码之外,我们将做任何事情 However, in the sample in MSDN and your own example, we will always wait at each of the await points. 但是,在MSDN中的示例和您自己的示例中,我们将始终等待每个await点。

So, we reach some point where we have await z and we've decided that we're going to wait. 所以,我们达到某种程度,我们await z ,我们决定等待。 This means a) That z hasn't Completed yet (whatever it means for z to be complete isn't something we care about at this moment in time) and b) That we have no useful work to do ourselves, at the moment. 这意味着a) z还没有完成(无论z是完整的意味着什么都不是我们在这个时刻所关心的事情)和b)我们现在没有任何有用的工作要做。

Hopefully, from the above, you can see why "creating a new thread" or anything like it isn't necessary, because like I just said, there's no useful work to do. 希望从上面可以看出为什么“创建一个新线程”或类似的东西是没有必要的,因为正如我刚才所说,没有什么有用的工作要做。

Before we return control from our method though, we're going to enqueue a continuation . 之前我们从我们的方法,虽然返回控制,我们要排队的延续 The async machinery is able to express " when z has completed, arrange for the rest of this method to continue from our await point". async机器能够表达“ z完成时,安排方法的其余部分从我们的await点继续”。

Here, things like synchronization contexts and ConfigureAwait become relevant. 在这里,同步上下文和ConfigureAwait类的东西变得相关。 The thread that we're running on (and that we're about to relinquish control of) may be "special" in some way. 我们正在运行的线程(以及我们即将放弃控制权)可能在某种程度上是“特殊的”。 It may be the UI thread. 它可能是UI线程。 It may currently have exclusive access to some resources (think ASP.Net pre-Core Request/Response/Session objects). 它目前可能拥有对某些资源的独占访问权(想想ASP.Net预核心请求/响应/会话对象)。

If that's so, hopefully the system providing the special-ness has installed a synchronization context, and it's via that that we also are able to obtain " when we resume execution of this method, we need to have the same special circumstances we previously had". 如果是这样的话,希望提供特殊功能的系统安装了一个同步上下文,并且通过它我们也能够获得“ 我们恢复执行这个方法时,我们需要具有我们之前所拥有的相同特殊情况” 。 So eg we're able to resume running our method back on the UI thread . 因此,例如,我们可以在UI线程上恢复运行我们的方法。

By default, without a synchronization context, a thread pool thread will be found to run our continuation. 默认情况下,如果没有同步上下文,将找到线程池线程来运行我们的延续。

Note that, if a method contains multiple await s that require waiting, after that first wait, we'll be running as a chained continuation. 注意,如果一个方法包含需要等待的多个await ,那么在第一次等待之后,我们将作为链式延续运行。 Our original caller got their context back the first time we await ed. 我们的原始来电者在我们一次await时获得了他们的背景。


In your sample, we have three await points and all three of them will wait and cause us to relinquish control back to our caller (and we don't have multiple await s in a single method to worry about either). 在你的样本中,我们有三个await点,并且它们中的所有三个等待并使我们放弃对我们的调用者的控制(并且我们没有多个await s在单个方法中担心)。 All of these awaits are, at the end of the day, waiting for that Thread.Sleep to finish (poor form in modern async code which should use TaskDelay instead). 所有这些等待都是在一天结束时,等待Thread.Sleep完成(现代async代码中的差形式应该使用TaskDelay )。 So any Console.WriteLine s appearing after an await in that method will be delayed. 因此, 在该方法中 await之后出现的任何Console.WriteLine都将被延迟。

However, M1 is async void , something best avoided outside of event handlers. 但是, M1async void ,在事件处理程序之外最好避免使用。 async void methods are problematic because they offer no means of determining when they've completed. async void方法存在问题,因为它们无法确定何时完成。

Main calls M1 which prints 1 and then calls M2 . Main调用M1打印1然后调用M2 M2 prints 2 and then calls M3 . M2打印2然后调用M3 M3 prints 4 , creates a new Task , prints 5 and only then can it relinquish control. M3打印4 ,创建一个新的Task ,打印5 ,然后才可以将放弃控制权。 It's at this point that it creates a TaskCompletionSource that will represent its eventual completion, obtain the Task from it, and return that. 在这一点上,它创建了一个TaskCompletionSource ,它将代表它的最终完成,从中获取Task ,并返回它。

Notice that it's only when the call to M3 returns that we hit the await in M2 . 请注意,只有当M3的调用返回时 ,我们才会在M2进行await We also have to wait here so we do almost the same as M3 and return a Task . 我们还必须在这里等待,这样我们几乎和M3一样,并返回一个Task

Now M1 finally has a Task that it can await on. 现在M1终于有了一个可以awaitTask But before it does that, it prints 3 . 但在此之前,它打印3 It returns control to Main which now prints 8 . 它将控制返回到Main ,现在打印8

Eons later, the Thread.Sleep in rs in M3 finishes running, and we print 6 . 亿万年后, Thread.SleeprsM3运行完毕,我们打印6 M3 marks its returned Task as complete, has no more work to do, and so exits with no further continuations arranged. M3其返回的Task标记为完成,没有更多的工作要做,因此退出时不再安排进一步的延续。

M2 can now resume and its await and print 6 . M2现在可以恢复, await并打印6 Having done that, its Task is complete and M1 can finally resume and print 7 . 完成后,其Task完成, M1最终可以恢复和打印7

M1 doesn't have a Task to mark as complete because it was async void . M1没有Task标记为完整,因为它是async void

It's easiest to see when you look at what this code is actually doing under the hood. 当你看到这段代码实际上在做什么时,最容易看到。 Here's an approximate translation of what that code is doing: 这是代码正在做的大致翻译:

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

Note that, for the sake of brevity, I've not done proper error handling here. 请注意,为了简洁起见,我没有在这里进行适当的错误处理。 One of the biggest benefits to using await is that it handles errors in the way you generally want them to, and using the tools I have here...doesn't. 使用await的最大好处之一是它以你通常想要的方式处理错误,并且使用我在这里的工具......没有。 This code also doesn't schedule the continuations to use the current synchronization context, something that's irrelevant to this situation, but is a useful feature. 此代码也没有安排继续使用当前同步上下文,这与这种情况无关,但是是一个有用的功能。

At this point it hopefully is more clear as to what's going on. 在这一点上,希望更清楚的是发生了什么。 For starters, M1 and M3 really have no business being async in the first place, as they never doing anything meaningful in their continuations. 对于初学者来说, M1M3确实没有业务首先是async ,因为他们从来没有做过任何有意义的延续。 They should really just not be async and to return the tasks they get from calling other methods. 它们实际上应该不是async并且返回它们从调用其他方法获得的任务。 M2 is the only method that actually does anything useful in any of the continuations it creates. M2是唯一在其创建的任何延续中实际执行任何有用的方法。

It also makes it clearer as to the order of operations. 它还使操作顺序更加清晰。 We call M1 , which calls M2 , which calls M3 , which calls Task.Run , then M3 returns a task that's identical to what Task.Run returns and then returns itself, then M2 adds its continuation and returns, then M3 executes the next printline, then adds its continuation (which does nothing when it eventually runs), then returns. 我们调用M1 ,调用M2 ,调用M3 ,调用Task.Run ,然后M3返回一个与Task.Run返回相同的任务,然后返回自身,然后M2添加其继续并返回,然后M3执行下一个printline ,然后添加它的延续(当它最终运行时什么都不做),然后返回。 Then at some later point in time the Task.Run finishes, and all of the continuations run, bottom up (because each is a continuation of the other). 然后在稍后的某个时间点, Task.Run完成,并且所有的延续都是自下而上的(因为每个都是另一个的延续)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM