C#中的语句之间是否存在延迟



我不知道如何解释我想问的问题,所以请考虑以下示例:

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指令)是。只有当您在关闭优化(即调试配置)的情况下编译代码时,这才是正确的,并且其目的是允许您逐步完成代码。查看它的最简单方法是,当使用DebugRelease目标编译它时,需要查看为方法生成的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迭代器移动到coloursIEnumerable中的新项目时,执行GetColours()的各个yield return语句。

最新更新