“switch”语句评估线程安全吗?

时间:2011-07-11 08:38:01

标签: c# thread-safety switch-statement atomicity

请考虑以下示例代码:

class MyClass
{
    public long x;

    public void DoWork()
    {
        switch (x)
        {
            case 0xFF00000000L:
                // do whatever...
                break;

            case 0xFFL:
                // do whatever...
                break;

            default:
                //notify that something going wrong
                throw new Exception();
        }
    }
}

忘记片段的无用性:我怀疑是switch陈述的行为。

假设x字段只能包含两个值:0xFF00000000L0xFFL。上面的开关不应该属于“默认”选项。

现在想象一个线程正在执行“x”等于0xFFL的开关,因此第一个条件将不匹配。同时,另一个线程将“x”变量修改为0xFF00000000L。我们知道64位操作不是原子操作,因此变量将首先将较低的dword置零,然后将较低的dword置零(反之亦然)。

如果在“x”为零时(即在新的分配期间)完成开关中的第二个条件,我们是否会陷入不受欢迎的“默认”情况?

4 个答案:

答案 0 :(得分:16)

是的,如您的问题所示,switch语句本身是线程安全的。字段x的值将一次加载到(隐藏的)局部变量中,并且该局部用于switch块。

不安全的是将字段x初始加载到局部变量中。 64位读取不保证是原子的,因此您可能会变得陈旧和/或那时撕裂的读数。这可以通过使用Interlocked.Read或类似方法轻松解决,以线程安全的方式将字段值显式读入本地:

long y = Interlocked.Read(ref x);
switch (y)
{
    // ...
}

答案 1 :(得分:12)

你实际上发了两个问题。

线程安全吗?

嗯,显然不是,另一个线程可能会在第一个线程进入交换机时更改X的值。由于没有锁定且变量不易变,因此您将根据错误的值进行切换。

您是否曾达到交换机的默认状态?

理论上你可能,因为更新64位不是原子操作,因此理论上你可以跳转到赋值的中间并得到x的混合结果,如你所指出的那样。这在统计上不会经常发生,但最终会发生。

交换机本身是线程安全的什么不是线程安全的读取和写入64位变量(在32位操作系统中)。

想象一下,您可以使用以下代码代替switch(x):

long myLocal = x;
switch(myLocal)
{
}

现在切换是在局部变量上进行的,因此它完全是线程安全的。当然,问题在于myLocal = x 读取,并且与其他作业存在冲突。

答案 2 :(得分:2)

C#的switch语句不被评估为一系列if条件(如VB所示)。 C#有效地构建了一个标签的哈希表,可以根据对象的值跳转到并直接跳转到正确的标签,而不是依次遍历每个条件并对其进行评估。

这就是为什么C#switch语句在增加案例数量时不会在速度方面恶化。这也是为什么C#在开关情况下比VB更具限制性的原因,例如,你可以在VB中进行数值范围。

因此,您没有所说的潜在竞争条件,进行比较,更改值,进行第二次比较等,因为只执行了一次检查。至于它是否完全是线程安全的 - 我不会这么认为。

通过IL中的C#switch语句查看反射器,您将看到发生了什么。将它与VB中的switch语句进行比较,其中包含值中的范围,您将看到差异。

我看了它已经有好几年了,所以事情可能会有些改变......

在此处查看有关switch语句行为的更多详细信息:Is there any significant difference between using if/else and switch-case in C#?

答案 3 :(得分:1)

正如您已经假设的那样,switch语句不是线程安全的,在某些情况下可能会失败。

此外,在您的实例变量上使用lock也不会有效,因为lock语句需要object导致您的实例变量被装箱。每次加载实例变量时,都会创建一个新的盒装变量,使lock无效。

在我看来,你有几个方案可以解决这个问题。

  1. 对任何引用类型的私有实例变量使用lockobject将完成工作)
  2. 使用ReaderWriterLockSlim让多个线程读取实例变量,但一次只有一个线程写入实例变量。
  3. 以实际方式将实例变量的值存储到方法中的局部变量(例如,使用Interlocked.ReadInterlocked.Exchange)并对局部变量执行switch。请注意,这样您可以使用switch的旧值。您必须确定这是否会导致您的具体用例出现问题。