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

时间:2015-07-29 15:54:51

标签: c# multithreading asynchronous async-await task-parallel-library

我实际上正在阅读有关任务并行库和使用async和await的异步编程的一些主题。这本书" C#5.0 in a Nutshell"声明在使用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!");
}

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

那么CLR如何确保按照订单的顺序处理请求?我怀疑编译器只是简单地将代码转换为上述方式,因为这会将“GetPrimesCountAsync”与“GetPrimesCountAsync”分离。来自for循环的方法。

2 个答案:

答案 0 :(得分:16)

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

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

    现在这个转换实际上并没有在C#代码中完成,它是在IL中完成的,但这有点像我上面在状态机中显示的代码的士气。请注意,这不是有效的C#(您不能goto进入这样的goto循环,但该限制不适用于实际使用的IL代码。也会有差异在这个和C#实际上做的事情之间,但是应该给你一个关于这里发生了什么的基本想法:

    for

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

答案 1 :(得分:7)

  

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

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

var myRunningTask = GetPrimesCountAsync();
await myRunningTask;

循环仅在等待任务完成时继续。从未有过多个任务。

  

那么CLR如何确保按照订单的顺序处理请求?

CLR没有参与。

  

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

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