是“开关”吗?语句求值是线程安全的吗?
考虑以下示例代码:
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 位操作不是原子操作,因此变量将首先将较低的双字清零,然后再将较高的双字清零(反之亦然)。
如果当“x”为零时(即在新分配期间)完成开关中的第二个条件,我们是否会陷入不希望的“默认”情况?
Consider the following sample code:
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();
}
}
}
Forget the uselessness of the snippet: my doubt is about the behavior of the switch
statement.
Suppose that the x
field could have only two values: 0xFF00000000L
or 0xFFL
. The switch above should not fall into the "default" option.
Now imagine that one thread is executing the switch with "x" equal to 0xFFL, thus the first condition won't match. At the same time, another thread modifies the "x" variable to 0xFF00000000L. We know a 64-bit operation is not atomic, so that the variable will have the lower dword zeroed first, then the upper set afterward (or vice versa).
If the second condition in the switch will be done when the "x" is zero (i.e. during the new assignment), will we fall into the undesired "default" case?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
是的,如您的问题所示,
switch
语句本身是线程安全的。字段x
的值被加载一次到(隐藏的)局部变量中,并且该局部变量用于switch
块。不安全的是字段
x
到局部变量的初始加载。64位读取不能保证是原子的,所以你可能会变得陈旧和/或当时的读物被撕毁。通过使用Interlocked.Read
< 可以轻松解决此问题/a> 或类似的方法,以线程安全的方式将字段值显式读取到本地:Yes, the
switch
statement itself, as shown in your question, is thread-safe. The value of fieldx
is loaded once into a (hidden) local variable and that local is used for theswitch
block.What isn't safe is the initial load of the field
x
into a local variable. 64-bit reads aren't guaranteed to be atomic, so you could be getting stale and/or torn reads at that point. This could easily be resolved by usingInterlocked.Read
, or similar, to explicitly read the field value into the local in a thread-safe way:您实际上发布了两个问题。
它是线程安全的吗?
嗯,显然不是,当第一个线程进入切换时,另一个线程可能会更改 X 的值。由于没有锁并且变量不是易失性的,因此您将根据错误的值进行切换。
你会选择开关的默认状态吗?
理论上你可以,因为更新 64 位不是原子操作,因此理论上你可以跳到赋值的中间并得到 x 的混合结果,正如你指出的那样。从统计数据来看,这种情况不会经常发生,但最终会发生。
但是开关本身是线程安全的,非线程安全的是读取和写入64位变量(在32位操作系统中)。
想象一下,您有以下代码而不是 switch(x):
现在 switch 是通过局部变量进行的,因此,它是完全线程安全的。当然,问题在于
myLocal = x
read 及其与其他分配的冲突。You're actually posting two questions.
Is it threadsafe?
Well, obviously it is not, another thread might change the value of X while the first thread is going into the switch. Since there's no lock and the variable is not volatile you'll switch based on the wrong value.
Would you ever hit the default state of the switch?
Theoretically you might, as updating a 64 bits is not an atomic operation and thus you could theoretically jump in the middle of the assignment and get a mingled result for x as you point out. This statistically won't happen often but it WILL happen eventually.
But the switch itself is threadsafe, what's not threadsafe is read and writes over 64 bit variables (in a 32 bit OS).
Imagine instead of switch(x) you have the following code:
now the switch is made over a local variable, and thus, it's completely threadsafe. The problem, of course, is in the
myLocal = x
read and its conflict with other assignments.C# 的 switch 语句不会被评估为一系列 if 条件(VB 可以如此)。 C# 根据对象的值有效地构建要跳转到的标签哈希表,并直接跳转到正确的标签,而不是依次迭代每个条件并对其进行评估。
这就是为什么 C# switch 语句不会随着案例数量的增加而降低速度。这也是为什么 C# 对 switch 情况下的比较限制比 VB 更严格,例如,您可以在 VB 中执行值范围。
因此,您不存在您所说的潜在竞争条件,即进行比较、值更改、进行第二次比较等,因为只执行了一项检查。至于它是否完全线程安全 - 我不这么认为。
使用 Reflector 进行挖掘,查看 IL 中的 C# switch 语句,您就会看到发生了什么。将其与 VB 中包含值范围的 switch 语句进行比较,您会发现差异。
自从我查看它以来已经有好几年了,所以事情可能已经发生了一些轻微的变化...
请在此处查看有关 switch 语句行为的更多详细信息:C# 中使用 if/else 和 switch-case 之间有什么显着区别吗?
C#'s switch statement isn't evaluated as a series of if conditions (as VB's can be). C# effectively builds up a hashtable of labels to jump to based on the value of the object and jumps straight to the correct label, rather than iterating through each condition in turn and evaluating it.
This is why a C# switch statement doesn't deteriorate in terms of speed as you increase the number of cases. And it's also why C# is more restrictive with what you can compare to in the switch cases than VB, in which you can do ranges of values, for example.
Therefore you don't have the potential race condition that you've stated, where a comparison is made, the value changes, the second comparison is made, etc, because there is only one check performed. As for whether it's totally threadsafe - I wouldn't assume so.
Have a dig with reflector looking through a C# switch statement in IL and you'll see what's happening. Compare it to a switch statement from VB which includes ranges in the values and you'll see a difference.
It's been quite a few years since I looked at it, so things may have changed slightly...
See more detail about switch statement behaviour here: Is there any significant difference between using if/else and switch-case in C#?
正如您已经假设的那样, switch 语句不是线程安全的,并且在某些情况下可能会失败。
此外,在实例变量上使用
lock
也不起作用,因为lock
语句需要一个object
导致实例变量被装箱。每次实例变量被装箱时,都会创建一个新的装箱变量,从而使锁
变得毫无用处。我认为您有多种选择来解决这个问题。
lock
(object
将完成这项工作)ReaderWriterLockSlim
让多个线程读取实例变量,但一次只有一个线程写入实例变量。Interlocked.Read
或Interlocked.Exchange
)并执行switch
局部变量。请注意,这样您就可以使用switch
的旧值。您必须确定这是否会在您的具体用例中引起问题。As you already assumed, the switch statement is not thread-safe and might fail in certain scenarios.
Furthermore, using
lock
on your instance variable won't work neither, because thelock
statement expects anobject
causing your instance variable to be boxed. Every time the instance variable is boxed, a new boxed variable will be created, making thelock
effectively useless.In my opinion you have several options solving this issue.
lock
on a private instance variable of any reference type (object
will do the job)ReaderWriterLockSlim
to let multiple threads read the instance variable but only one thread write the instance variable at a time.Interlocked.Read
orInterlocked.Exchange
) and perform theswitch
on the local variable. Note that this way you might use the old value for theswitch
. You have to decide if this can cause problems in your concrete use-case.