【问题标题】:Async and Await - How is order of execution maintained?异步和等待 - 如何维护执行顺序?
【发布时间】:2015-10-20 17:26:13
【问题描述】:

我实际上正在阅读一些关于任务并行库以及使用 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!");
}

“GetPrimesCountAsync”方法的调用将被排入队列并在池线程上执行。通常,从 for 循环中调用多个线程可能会引入竞争条件。

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

【问题讨论】:

  • 它创建了一个状态机。此外,该代码不会并行运行,它只会在等待每个对 GetPrimesCountAsync 的调用按顺序完成时释放线程。

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


【解决方案1】:

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

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&lt;T&gt;,但它所做的工作将在后台继续进行。
  5. 方法的其余部分作为返回任务的延续添加;该任务完成后它将继续执行。
  6. SomeExpensiveComputation返回的任务完成后,我们将结果存储在value中。
  7. value 打印到控制台。
  8. 转到 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();
    }
}

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

【讨论】:

  • 感谢您的回答。据我了解,for 循环的处理速度非常快,因此创建了 10 个等待者(任务)来等待。这里唯一的延续是对“Console.WriteLine”的调用。那么CLR如何保证只有在Task完成后才会继续for循环呢?
  • 因为这就是 awaiter 的 OnCompleted 方法的工作原理。只有当它代表的操作完成时才会触发它。在这种情况下,等待者是 Task 等待者,所以实际上它只是在它所代表的 Task 实际完成之后调用方法的 Task 类的实现,而不是在那之前。它与 CLR 无关。
  • 我想我现在明白了。整个 for 循环(以及它的状态)是延续的一部分。因此,直到每个等待者一步一步完成,for 循环才会继续。这和我想的完全一样,因为只有这样,for循环的状态才会嵌入到continuation中,从而允许任务以正确的顺序执行。
【解决方案2】:

“GetPrimesCountAsync”方法的调用将被排入队列并在池线程上执行。

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

var myRunningTask = GetPrimesCountAsync();
await myRunningTask;

只有在等待的任务完成后,循环才会继续。完成的任务永远不会超过一项。

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

CLR 不参与。

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

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

【讨论】:

  • 谢谢。这也是一个非常有帮助的答案。你是对的:await 关键字只期望实现“INotifyCompletion”的对象完成。在线程池线程上执行它取决于“GetResult”的实现。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-06-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-12-30
  • 2020-09-11
相关资源
最近更新 更多