对C#的异步/等待控制流感到困惑

时间:2018-12-10 13:18:21

标签: c# async-await

我正在学习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;
    }
}

此演示打印一些表示执行流程的标签,我认为应该是

STEP:1

STEP:2

STEP:3

STEP:4

STEP:5

STEP:6

STEP:7

STEP:8

(按顺序从1到8)

,但实际结果是: Figure 1

STEP:1

STEP:2

STEP:4

STEP:5

STEP:3

STEP:8

STEP:6

STEP:7

如果像MSDN的说明一样,控制权返回到M1并打印“ STEP 3”,那绝对是我错的,那到底是怎么回事?

预先感谢

2 个答案:

答案 0 :(得分:6)

仅是为了解决问题,我们无法保证在任何特定的await时刻,我们将继续进行 之外的其他代码。但是,在MSDN中的示例和您自己的示例中,我们将始终在每个await点等待。

因此,我们到达await z的某个位置,因此我们决定要等待。这意味着a)z尚未完成(z完成并不意味着我们目前不在乎此事)b)我们没有任何有用的工作现在做自己。

希望从上面可以看出,为什么不需要“创建新线程”或诸如此类的事情,因为就像我刚才所说的,没有要做的有用工作。

在我们从方法中返回控制之前,我们要加入 continuation async机制能够表达“ z结束后,请安排 this 方法的其余部分从我们的await点开始”。

在这里,诸如同步上下文和ConfigureAwait之类的东西变得很重要。我们正在运行的线程(以及我们将要放弃控制的线程)在某种程度上可能是“特殊的”。它可能是UI线程。它目前可能具有对某些资源的独占访问权限(请考虑ASP.Net核心前请求/响应/会话对象)。

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

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

请注意,如果一个方法包含多个需要等待的await,则在第一次等待之后,我们将以链状连续体的形式运行。我们的原始调用方在我们await编辑的第一次 中获得了上下文。


在您的示例中,我们有3个await点,并且所有3个 将等待并导致我们将控制权交还给调用方(而且我们没有多个{{ 1}} s中的一种来担心这两种情况。最后,所有这些等待都在等待await完成(现代Thread.Sleep代码中格式较差,应改用async)。因此,在该方法中,在TaskDelay 之后出现的所有Console.WriteLine都会被延迟。

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

async void调用Main,它打印M1,然后调用1M2打印M2,然后调用2M3打印M3,创建新的4,打印Task,然后 it 才能放弃控制。此时,它将创建一个5来表示其最终完成,并从中获取TaskCompletionSource并返回它。

请注意,只有在返回Task的呼叫 时,我们才能击中M3中的await。我们还必须在这里等待,因此我们几乎与M2相同,并返回M3

现在Task终于有了M1可以使用的Task。但在此之前,它会打印await。它将控制权返回给3,现在将打印Main

之后,8Thread.Sleep中的rs运行完毕,我们打印了M36将返回的M3标记为已完成,没有更多工作要做,因此退出时没有安排任何继续。

Task现在可以恢复并恢复其M2并打印await。完成此操作后,其6就完成了,Task最终可以恢复并打印M1

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

答案 1 :(得分: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完成,并且所有延续都自下而上运行(因为每个都是另一个的延续)。