我不知道如何解释我想问的问题,所以请考虑以下示例:
int number;
void ChangeNumber()
{
number++;
UseNumber(number);
}
void UseNumber(int value)
{
// use number
}
我使用这个简单的例子来尝试找出当运行脚本时,它是否会等待一个任务完成,然后再转到下一个任务。例如,UseNumber(数字)是否仅在数字之前递增时调用?
考虑一下,UseNumber(number)在被调用之前还会等待所有这些运行吗?如果没有,回调是否是一个很好的选择,以确保之前的任务首先完成?
简单的答案是:C#行为合理;您可以保证,后面的语句会观察到前面语句的效果。
长期的答案是:当你看到更复杂的情况时,事情会比这更微妙。
首先:C#不能保证代码实际运行的顺序与它在程序中出现的顺序相同。所做的保证比这更微妙:该语言保证程序将显示为运行,就像它按照程序中代码出现的顺序运行一样。
这种微妙之处可能需要更多的解释。假设你有:
int foo = 1;
int bar = 2;
foo += 1;
bar += 1;
Console.WriteLine(foo);
Console.WriteLine(bar);
这个程序必须在控制台写入之前增加foo和bar,并且控制台写入必须先发生foo,然后发生bar。然而,上面程序片段的行为与相同
int foo = 1;
int bar = 2;
bar += 1;
foo += 1;
Console.WriteLine(foo);
Console.WriteLine(bar);
它的行为与这个程序片段相同:
Console.WriteLine(2);
Console.WriteLine(3);
由于在每种情况下观察到的副作用都是相同的,因此允许C#运行这些程序中的任何,而不是最初编写的代码。
当你添加时,事情会变得更加复杂
- 迭代程序块
- 异步工作流
- 并发
- 非托管代码互操作
- 定稿
当你加入这些限制时,对C#如何保证保持效果排序的限制会变得非常复杂;描述其中任何一个都可能是一个非常长的答案,所以如果你有问题,试着问第二个更集中的问题。
C#与大多数编程语言一样,同步运行所有语句。这基本上等同于说所有语句都按照遇到的顺序执行,一次执行一个。
然而,C#支持异步编程,在这一点上,程序员负责等待什么。
因此,是的,在发布UseNumber
的示例中,只有在number++
完成时才会调用它。
程序可以在编译和运行时执行期间进行优化和/或重新排序。或者可能存在小的";"延迟";执行期间但是,规范仍然保证"预期"的订购。
根据规范(ECMA-334/2017):
8.10执行订单
C#程序的执行继续进行,以便在关键执行点保留每个执行线程的副作用[(例如写入变量)]副作用定义为读取或写入易失性字段、写入非易失性变量、写入外部资源以及抛出异常。。
•数据依赖性保留在执行线程中也就是说,计算每个变量的值时,就好像线程中的所有语句都是按原始程序顺序执行的一样
•初始化顺序规则保留[执行线程之间]。。
•对于易失性读写,保留了副作用(执行线程之间)的顺序。。
强保证(粗体)适用于单个执行线程。多个线程可以交错执行,并引入各种[意外]的竞争条件。但是,每个线程的执行顺序都有很好的定义。
async/await
关键字是语法糖(用于任务),并且是该保证执行顺序的补充。使用async/await
(这是并发模型)和/或线程是否合适,实际上取决于"UseNumber"的作用。对于这种情况,还没有给出合理的解释,并发性/并行性的虚假使用增加了和开销的复杂性。
添加到其他答案中,这些答案似乎集中在所有同步操作是如何同步的。实际上,编译器在调用之间插入的延迟(或者更确切地说是ILnop
指令)是。只有当您在关闭优化(即调试配置)的情况下编译代码时,这才是正确的,并且其目的是允许您逐步完成代码。查看它的最简单方法是,当使用Debug
和Release
目标编译它时,需要查看为方法生成的IL。
// optimisations off
IL_0000: nop
IL_0001: nop
IL_0002: nop
IL_0003: ret
<Main>g__ChangeNumber|0_0:
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldfld UserQuery+<>c__DisplayClass0_0.number
IL_0007: stloc.0
IL_0008: ldarg.0
IL_0009: ldloc.0
IL_000A: ldc.i4.1
IL_000B: add
IL_000C: stfld UserQuery+<>c__DisplayClass0_0.number
IL_0011: ldarg.0
IL_0012: ldfld UserQuery+<>c__DisplayClass0_0.number
IL_0017: call UserQuery.<Main>g__UseNumber|0_1
IL_001C: nop
IL_001D: ret
<Main>g__UseNumber|0_1:
IL_0000: nop
IL_0001: ret
并将其与调试标志设置为时的情况进行比较
IL_0000: ret
<Main>g__ChangeNumber|0_0:
IL_0000: ldarg.0
IL_0001: ldfld UserQuery+<>c__DisplayClass0_0.number
IL_0006: stloc.0
IL_0007: ldarg.0
IL_0008: ldloc.0
IL_0009: ldc.i4.1
IL_000A: add
IL_000B: stfld UserQuery+<>c__DisplayClass0_0.number
IL_0010: ldarg.0
IL_0011: ldfld UserQuery+<>c__DisplayClass0_0.number
IL_0016: call UserQuery.<Main>g__UseNumber|0_1
IL_001B: ret
<Main>g__UseNumber|0_1:
IL_0000: ret
是。像您描述的那些简单语句是一个接一个地执行的,所以执行会"等待"其中一个语句完成,然后再转到下一个。
也就是说,C#确实支持并发。您可以启动不同的线程,或者更好的是同时运行的任务。对于不阻塞的执行,您还可以定义异步函数。查看这篇文章以获取简介。
语句顺序可以改变的另一个例子是通过迭代器块实现的延迟执行。考虑以下代码:
void DisplayColours()
{
var colours = GetColours();
foreach (var colour in colours)
{
Console.WriteLine($"The next colour is: {colour}");
}
}
IEnumerable<string> GetColours()
{
yield return "red";
yield return "green";
yield return "blue";
}
在上面的示例中,当您调用GetColours()
将其分配给colours
时,GetColours()
中的任何代码都不会执行。每当foreach
迭代器移动到colours
IEnumerable
中的新项目时,执行GetColours()
的各个yield return
语句。