基于C#7元组的变量交换线程是否安全?

时间:2018-01-31 19:53:51

标签: c# thread-safety

在C#7的元组之前,交换两个变量的标准方法是:

var foo = 5;
var bar = 10;

var temp = foo;
foo = bar;
bar = temp;

但现在我们可以使用

(foo, bar) = (bar, foo);

它在一条线上,它更漂亮。但它是否是线程安全的 - 是原子上完成的交换,还是只是在多步操作之上的糖?

3 个答案:

答案 0 :(得分:1)

“不,基本上”。

ValueTuple<...>系列是可变值类型,这使得它很复杂。较旧的Tuple<...>系列是不可变引用类型; “不可变”很重要,因为它意味着它不会改变单个字段 - 它正在创建一个带有所有值的新对象。 “引用类型”很重要,因为这是一个单独的引用交换, 线程安全,因为你无法获得“撕裂引用”。它在其他方面不是线程安全的:不保证订购或寄存器等。

但是ValueTuple<...>即使这样也消失了。因为它是一种可变类型,所以很可能实现为多个ldloca / ld... / stfld指令,所以即使值类型不大于CPU宽度,并不能保证它将全部写入单个CPU指令 - 而且几乎肯定不会。在“返回值,分配整个事物”场景中,如果足够小,可能是单个CPU指令,但它可能不会!为了使它更加复杂,另外到可变字段方法,还有自定义构造函数 - 但它们仍将最终写入相同的内存位置(对于值)类型,目标托管引用将传递给构造函数,而不是传递 out 的构造值。

语言或运行时无法保证元组的原子性;他们只保证引用和某些原语 - 即使是 很多很多

最后,依赖目标CPU;显然,一个2-int元组不能在32位CPU上是原子的。

答案 1 :(得分:1)

您的第一种方法编译为:

.method private hidebysig static 
    void Method1 () cil managed 
{
    // Method begins at RVA 0x2068
    // Code size 13 (0xd)
    .maxstack 1
    .locals init (
        [0] int32,
        [1] int32,
        [2] int32
    )

    // (no C# code)
    IL_0000: nop
    // int num = 5;
    IL_0001: ldc.i4.5
    IL_0002: stloc.0
    // int num2 = 10;
    IL_0003: ldc.i4.s 10
    IL_0005: stloc.1
    // int num3 = num;
    IL_0006: ldloc.0
    IL_0007: stloc.2
    // num = num2;
    IL_0008: ldloc.1
    IL_0009: stloc.0
    // num2 = num3;
    IL_000a: ldloc.2
    IL_000b: stloc.1
    // (no C# code)
    IL_000c: ret
} // end of method Program::Method1

你的第二种方法编译为:

.method private hidebysig static 
    void Method2 () cil managed 
{
    // Method begins at RVA 0x2084
    // Code size 13 (0xd)
    .maxstack 2
    .locals init (
        [0] int32,
        [1] int32,
        [2] int32
    )

    // (no C# code)
    IL_0000: nop
    // int num = 5;
    IL_0001: ldc.i4.5
    IL_0002: stloc.0
    // int num2 = 10;
    IL_0003: ldc.i4.s 10
    IL_0005: stloc.1
    // int num3 = num2;
    IL_0006: ldloc.1
    // int num4 = num;
    IL_0007: ldloc.0
    IL_0008: stloc.2
    // num = num3;
    IL_0009: stloc.0
    // num2 = num4;
    IL_000a: ldloc.2
    IL_000b: stloc.1
    // (no C# code)
    IL_000c: ret
} // end of method Program::Method2

正如您所看到的,他们的编译方式略有不同,但有效地采取了类似的步骤来实现相同的目标。

一个调用Load-Store-Load-Store-Load-Store,另一个调用Load-Load-Store-Store-Load-Store。

这里唯一有趣的注意事项是元组版本分配额外的内存,因为它一次在堆栈上存储两个项目,而第一种方法在任何给定时间只在堆栈中存储一个项目。“ p>

答案 2 :(得分:1)

扩展之前的答案......

不,不是线程安全的,但请记住每个线程都有自己的局部变量,除非你交换共享的东西,否则你不必担心。

如果要交换共享值,那么可以使用许多技术使其保持线程安全。你可以使用锁。您可以将多个变量放入对象并以原子方式交换对象。您可以使用.NET并发容器。这取决于你想要做的具体细节。