异步和等待 - 如何维护执行顺序?

Den*_*sel 8 c# multithreading asynchronous task-parallel-library async-await

我实际上正在阅读有关任务并行库和使用async和await的异步编程的一些主题."Nutshell中的C#5.0"一书指出,在使用await关键字等待表达式时,编译器会将代码转换为如下所示:

var awaiter = expression.GetAwaiter();
awaiter.OnCompleted (() =>
{
var result = awaiter.GetResult();
Run Code Online (Sandbox Code Playgroud)

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

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!");
}
Run Code Online (Sandbox Code Playgroud)

"GetPrimesCountAsync"方法的调用将在池化线程上排队并执行.通常,在for循环中调用多个线程有可能引入竞争条件.

那么CLR如何确保请求按照它们的顺序进行处理?我怀疑编译器只是将代码转换为上述方式,因为这会将'GetPrimesCountAsync'方法与for循环分离.

Ser*_*rvy 18

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

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

由于代码的定义,所有订单都保持不变.让我们想象一下它.

  1. 首先调用此方法
  2. 第一行代码是for循环,因此i被初始化.
  3. 循环检查通过,所以我们转到循环体.
  4. SomeExpensiveComputation叫做.它应该Task<T>很快返回,但它所做的工作将继续在后台进行.
  5. 该方法的其余部分作为返回任务的延续添加; 该任务完成后它将继续执行.
  6. SomeExpensiveComputation完成返回任务后,我们将结果存储在value.
  7. value 打印到控制台.
  8. GOTO 3; 请注意,在我们第二次进入第4步并开始下一步之前,现有的昂贵操作已经完成.

至于C#编译器如何实际完成第5步,它是通过创建状态机来实现的.基本上每当有await一个标签指示它停止的位置时,以及在方法开始时(或在任何继续触发之后它恢复之后),它会检查当前状态,并执行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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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


usr*_*usr 7

"GetPrimesCountAsync"方法的调用将在池化线程上排队并执行.

await不会启动任何后台处理.它等待现有的处理完成.这样GetPrimesCountAsync做(例如使用Task.Run).这种方式更清楚:

var myRunningTask = GetPrimesCountAsync();
await myRunningTask;
Run Code Online (Sandbox Code Playgroud)

循环仅在等待任务完成时继续.永远不会有多个任务未完成.

那么CLR如何确保请求按照它们的顺序进行处理?

CLR没有参与.

我怀疑编译器只是将代码转换为上述方式,因为这会将'GetPrimesCountAsync'方法与for循环分离.

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