Async和Await——执行顺序是如何维护的

本文关键字:维护 何维护 Await 执行 顺序 Async | 更新日期: 2023-09-27 18:05:19

我实际上正在阅读一些关于任务并行库和异步编程与async和await的主题。《c# 5.0概论》一书指出,当使用await关键字等待表达式时,编译器会将代码转换为如下内容:

var awaiter = expression.GetAwaiter();
awaiter.OnCompleted (() =>
{
var result = awaiter.GetResult();

让我们假设,我们有这个异步函数(也来自参考书):

async Task DisplayPrimeCounts()
{
for (int i = 0; i < 10; i++)
Console.WriteLine (await GetPrimesCountAsync (i*1000000 + 2, 1000000) +
" primes between " + (i*1000000) + " and " + ((i+1)*1000000-1));
Console.WriteLine ("Done!");
}

GetPrimesCountAsync方法的调用将在一个池线程中排队并执行。一般来说,在for循环中调用多个线程可能会引入竞争条件。

那么CLR如何确保请求按照它们发出的顺序被处理呢?我怀疑编译器是否会简单地将代码转换为上述方式,因为这会将'GetPrimesCountAsync'方法与for循环解耦。

Async和Await——执行顺序是如何维护的

为了简单起见,我将把您的示例替换为一个稍微简单一些的示例,但具有所有相同的有意义的属性:

async Task DisplayPrimeCounts()
{
    for (int i = 0; i < 10; i++)
    {
        var value = await SomeExpensiveComputation(i);
        Console.WriteLine(value);
    }
    Console.WriteLine("Done!");
}

由于代码的定义,所有顺序都保持不变。

  1. 这个方法首先被调用
  2. 第一行代码是for循环,所以i被初始化。
  3. 循环检查通过,所以我们进入循环体。
  4. 调用
  5. SomeExpensiveComputation。它应该很快返回一个Task<T>,但是它所做的工作将继续在后台进行。
  6. 方法的其余部分作为返回任务的延续添加;当该任务完成后,它将继续执行。
  7. SomeExpensiveComputation返回的任务完成后,我们将结果存储在value中。
  8. value被打印到控制台。
  9. GOTO 3;请注意,在我们第二次进入步骤4并开始下一个操作之前,现有的昂贵操作已经完成。
至于c#编译器实际上是如何完成第5步的,它是通过创建一个状态机来完成的。基本上,每次出现await时,都会有一个标签指示它停止的位置,并且在方法开始时(或在任何continuation触发后恢复后),它会检查当前状态,并对它停止的位置执行goto。它还需要将所有局部变量提升到一个新类的字段中,以便维护这些局部变量的状态。

现在这个转换实际上不是在c#代码中完成的,它是在IL中完成的,但这是我上面在状态机中展示的代码的士气等效物。请注意,这在c#中是无效的(您不能像这样将goto放入一个for循环中,但该限制不适用于实际使用的IL代码。这与c#的实际功能之间也存在差异,但它应该让你对这里发生的事情有一个基本的了解:

internal class Foo
{
    public int i;
    public long value;
    private int state = 0;
    private Task<int> task;
    int result0;
    public Task Bar()
    {
        var tcs = new TaskCompletionSource<object>();
        Action continuation = null;
        continuation = () =>
        {
            try
            {
                if (state == 1)
                {
                    goto state1;
                }
                for (i = 0; i < 10; i++)
                {
                    Task<int> task = SomeExpensiveComputation(i);
                    var awaiter = task.GetAwaiter();
                    if (!awaiter.IsCompleted)
                    {
                        awaiter.OnCompleted(() =>
                        {
                            result0 = awaiter.GetResult();
                            continuation();
                        });
                        state = 1;
                        return;
                    }
                    else
                    {
                        result0 = awaiter.GetResult();
                    }
                state1:
                    Console.WriteLine(value);
                }
                Console.WriteLine("Done!");
                tcs.SetResult(true);
            }
            catch (Exception e)
            {
                tcs.SetException(e);
            }
        };
        continuation();
    }
}

请注意,为了这个示例,我忽略了任务取消,忽略了捕获当前同步上下文的整个概念,还有一些错误处理等。不要认为这是一个完整的实现

GetPrimesCountAsync方法的调用将被排队并在一个池线程中执行。

await不启动任何类型的后台处理。它等待现有的处理完成。这是由GetPrimesCountAsync来做的(例如使用Task.Run)。这样更清楚:

var myRunningTask = GetPrimesCountAsync();
await myRunningTask;

循环只在等待任务完成时继续。永远不会有超过一个任务未完成。

那么CLR如何确保请求按照它们发出的顺序被处理呢?

不涉及CLR。

我怀疑编译器会简单地将代码转换成上述方式,因为这会将'GetPrimesCountAsync'方法与for循环解耦。

您显示的转换基本上是正确的,但是请注意下一个循环迭代没有立即开始,而是在回调中。这就是序列化执行