异步和等待 - 如何维护执行顺序?
Async and Await - How is order of execution maintained?
我实际上正在阅读一些关于任务并行库和使用 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 循环分离。
为了简单起见,我将用稍微简单但具有所有相同有意义属性的示例替换您的示例:
async Task DisplayPrimeCounts()
{
for (int i = 0; i < 10; i++)
{
var value = await SomeExpensiveComputation(i);
Console.WriteLine(value);
}
Console.WriteLine("Done!");
}
由于您的代码定义,排序全部保持不变。让我们想象一下逐步完成它。
- 这个方法第一次被调用
- 第一行代码是for循环,所以
i
被初始化。
- 循环检查通过,所以我们进入循环体。
SomeExpensiveComputation
被调用。它应该 return 一个 Task<T>
非常快,但它所做的工作将在后台继续进行。
- 该方法的其余部分作为 returned 任务的延续添加;它将在该任务完成后继续执行。
- 从
SomeExpensiveComputation
编辑的任务 return 完成后,我们将结果存储在 value
中。
value
打印到控制台。
- 转到3;请注意,在我们第二次进入第 4 步并开始下一个之前,现有的昂贵操作已经完成。
就 C# 编译器实际完成步骤 5 的方式而言,它是通过创建状态机来实现的。基本上每次有一个 await
都有一个标签指示它从哪里停止,并且在方法开始时(或在任何延续触发后恢复之后)它检查当前状态,并执行 goto
到它停止的地方。它还需要将所有局部变量提升到新的 class 字段中,以便维护这些局部变量的状态。
现在这个转换实际上不是在 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();
}
}
请注意,为了这个示例,我忽略了任务取消,我忽略了捕获当前同步上下文的整个概念,还有更多错误处理等。不要考虑这是一个完整的实现。
The call of the 'GetPrimesCountAsync' method will be enqueued and executed on a pooled thread.
没有。 await
不启动任何类型的后台处理。它等待现有处理完成。这取决于 GetPrimesCountAsync
(例如使用 Task.Run
)。这样更清楚:
var myRunningTask = GetPrimesCountAsync();
await myRunningTask;
只有等待的任务完成后,循环才会继续。未完成的任务永远不会超过一项。
So how does the CLR ensure that the requests will be processed in the order they were made?
不涉及 CLR。
I doubt that the compiler simply transforms the code into the above manner, since this would decouple the 'GetPrimesCountAsync' method from the for loop.
您显示的转换基本上是正确的,但请注意下一个循环迭代不会立即开始,而是在回调 中开始。这就是序列化执行。
我实际上正在阅读一些关于任务并行库和使用 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 循环分离。
为了简单起见,我将用稍微简单但具有所有相同有意义属性的示例替换您的示例:
async Task DisplayPrimeCounts()
{
for (int i = 0; i < 10; i++)
{
var value = await SomeExpensiveComputation(i);
Console.WriteLine(value);
}
Console.WriteLine("Done!");
}
由于您的代码定义,排序全部保持不变。让我们想象一下逐步完成它。
- 这个方法第一次被调用
- 第一行代码是for循环,所以
i
被初始化。 - 循环检查通过,所以我们进入循环体。
SomeExpensiveComputation
被调用。它应该 return 一个Task<T>
非常快,但它所做的工作将在后台继续进行。- 该方法的其余部分作为 returned 任务的延续添加;它将在该任务完成后继续执行。
- 从
SomeExpensiveComputation
编辑的任务 return 完成后,我们将结果存储在value
中。 value
打印到控制台。- 转到3;请注意,在我们第二次进入第 4 步并开始下一个之前,现有的昂贵操作已经完成。
就 C# 编译器实际完成步骤 5 的方式而言,它是通过创建状态机来实现的。基本上每次有一个 await
都有一个标签指示它从哪里停止,并且在方法开始时(或在任何延续触发后恢复之后)它检查当前状态,并执行 goto
到它停止的地方。它还需要将所有局部变量提升到新的 class 字段中,以便维护这些局部变量的状态。
现在这个转换实际上不是在 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();
}
}
请注意,为了这个示例,我忽略了任务取消,我忽略了捕获当前同步上下文的整个概念,还有更多错误处理等。不要考虑这是一个完整的实现。
The call of the 'GetPrimesCountAsync' method will be enqueued and executed on a pooled thread.
没有。 await
不启动任何类型的后台处理。它等待现有处理完成。这取决于 GetPrimesCountAsync
(例如使用 Task.Run
)。这样更清楚:
var myRunningTask = GetPrimesCountAsync();
await myRunningTask;
只有等待的任务完成后,循环才会继续。未完成的任务永远不会超过一项。
So how does the CLR ensure that the requests will be processed in the order they were made?
不涉及 CLR。
I doubt that the compiler simply transforms the code into the above manner, since this would decouple the 'GetPrimesCountAsync' method from the for loop.
您显示的转换基本上是正确的,但请注意下一个循环迭代不会立即开始,而是在回调 中开始。这就是序列化执行。