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