考虑以下示例代码:
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
字段只能有两个值:0xFF00000000L
或0xFFL
。上面的开关不应该属于"default"选项。
现在想象一个线程正在执行"x"等于0xFFL的开关,因此第一个条件不匹配。同时,另一个线程将"x"变量修改为0xFF00000000L。我们知道64位操作不是原子操作,因此该变量将先将较低的双字归零,然后再将较高的双字归零(反之亦然)。
如果开关中的第二个条件将在"x"为零时完成(即在新的赋值期间),我们会陷入不希望的"默认"情况吗?
是的,switch
语句本身,如您的问题所示,是线程安全的。字段x
的值被加载到一次到一个(隐藏的)局部变量中,该局部变量用于switch
块。
不安全的是将字段x
初始加载到局部变量中。 64位读不能保证是原子性的,因此此时您可能会得到过时和/或撕裂的读。这可以很容易地通过使用Interlocked.Read
或类似的方法来解决,以线程安全的方式显式地将字段值读入本地:
long y = Interlocked.Read(ref x);
switch (y)
{
// ...
}
你实际上发布了两个问题。
是线程安全的吗?
显然不是,当第一个线程进入switch时,另一个线程可能会改变X的值。因为没有锁,而且变量不是易失的,所以你会根据错误的值进行切换。
你会碰到开关的默认状态吗?
理论上是可以的,因为更新64位不是原子操作,所以理论上你可以跳到赋值的中间,得到x的混合结果。从统计数据来看,这种情况不会经常发生,但最终会发生的。
但是开关本身是threadsafe的, 不是threadsafe的是读写64位变量(在32位操作系统中)。
假设你有以下代码而不是switch(x):
long myLocal = x;
switch(myLocal)
{
}
现在在一个局部变量上进行切换,因此,它是完全线程安全的。当然,问题在于myLocal = x
read及其与其他赋值的冲突。
c#的switch语句不是作为一系列if条件计算的(而VB可以)。c#根据对象的值有效地构建了一个标签哈希表,并直接跳转到正确的标签,而不是依次遍历每个条件并求值。
这就是为什么c# switch语句不会随着案例数量的增加而减慢速度的原因。这也是为什么c#比VB更限制你在切换情况下可以比较的东西,例如,在VB中你可以做值的范围。
因此,你没有潜在的竞争条件,你已经说过,在比较中,值改变,第二次比较,等等,因为只执行了一次检查。至于它是否完全是线程安全的,我不这么认为。
在IL中使用反射器查看c# switch语句,您将看到发生了什么。将其与VB中的switch语句进行比较,其中包含值的范围,您将看到差异。
我已经好几年没看它了,所以情况可能略有变化…
在c#中使用if/else和switch-case有什么显著的区别吗?正如您已经假设的那样,switch语句不是线程安全的,在某些情况下可能会失败。
此外,在实例变量上使用lock
也不起作用,因为lock
语句期望object
导致实例变量被装箱。每次实例变量被装箱,一个新的装箱变量将被创建,使lock
有效无用。
在我看来,你有几个选择来解决这个问题。
- 在任何引用类型的私有实例变量上使用
lock
(object
将完成这项工作) - 使用
ReaderWriterLockSlim
允许多个线程读取实例变量,但一次只有一个线程写入实例变量。 - 将实例变量的值自动存储到方法中的局部变量中(例如使用
Interlocked.Read
或Interlocked.Exchange
),并在局部变量上执行switch
。注意,这样您可能会使用switch
的旧值。您必须确定这是否会在您的具体用例中导致问题。