当 Interlocked 类可用时,为啥在 .NET 中使用 SyncLocks 进行简单操作?

Posted

技术标签:

【中文标题】当 Interlocked 类可用时,为啥在 .NET 中使用 SyncLocks 进行简单操作?【英文标题】:Why use SyncLocks in .NET for simple operations when Interlocked class is available?当 Interlocked 类可用时,为什么在 .NET 中使用 SyncLocks 进行简单操作? 【发布时间】:2010-03-28 21:03:31 【问题描述】:

我在 VB.NET 中做简单的多线程已经有一段时间了,并且刚刚进入我的第一个大型多线程项目。我一直使用Synclock 语句完成所有操作,因为我认为没有更好的方法。

我刚刚了解了 Interlocked 类 - 它使它看起来好像这一切:

Private SomeInt as Integer
Private SomeInt_LockObject as New Object

Public Sub IntrementSomeInt
    Synclock SomeInt_LockObject
        SomeInt += 1
    End Synclock
End Sub

可以用单个语句替换:

Interlocked.Increment(SomeInt)

这会在内部处理所有锁定并修改数字。这比为简单操作编写自己的锁要简单得多(运行时间更长或更复杂的操作显然仍然需要自己的锁)。

当我可以使用Interlocked 方法完成相同的事情时,我是否有理由使用专用锁定对象滚动自己的锁定?

【问题讨论】:

【参考方案1】:

你是对的;这里应该使用Interlocked,会比SyncLock快。 但是,Interlocked 类并不为人所知。

但是,在某些情况下,您需要使用 SyncLockInterlocked 并没有帮助。

【讨论】:

你能举一个这种情况的例子吗?如果我只是做一个简单的递增/递减(或者,如果我需要将值更改超过 1,则使用 .Add),看起来 Interlocked 在所有情况下都会更好。如果我需要更高级的东西(比如一个 4 步过程,我需要保证它是在任何给定时间运行它的唯一线程),我需要一些同步。我对 Interlocked 似乎是合适的情况感兴趣,但这是一个糟糕的选择。 如果您使用两个不同的变量,Interlocked 是不够的。【参考方案2】:

这是因为没有人知道它。传播这个词!

【讨论】:

我就是这么想的——我看到它从 1.0 就已经存在了,所以当我刚找到它时我很震惊。我想“这里肯定发生了我没有看到的其他事情,否则这种锁定方法会更受欢迎”。我真的只是想确保它第一眼看上去就很棒。 这个答案没有传达足够的信息,也没有直接回答问题。【参考方案3】:

简短的回答是因为使用Monitor 锁(VB 中的SyncLock 和C# 中的lock )不仅可以确保一次只有一个线程可以访问该变量(或者,严格来说,只有一次一个线程可以获得锁定对象的锁定),但它也会创建所需的内存屏障,以确保不会优化对变量的读取。

如果您从不简单地读取变量的值(换句话说,您的所有工作都是通过调用Interlocked 完成的),那么您就可以了。但是,如果您需要能够对变量执行正常读取,则情况会更加复杂。无锁读/写通常在 C# 中使用 volatile 关键字完成。这指示编译器在使用它的任何地方读取变量的值,而不是将这些读取中的任何一个优化到本地缓存中。不幸的是,在 VB.NET 中没有等价物,所以你必须使用其他东西。

this question 的已接受答案应提供有关您可以做什么的更多信息。简而言之,大多数人在 VB.NET 中使用SyncLock,因为它比不使用SyncLock 所需的逻辑更容易且更简单

【讨论】:

不完全正确,当您使用 Interlocked 更改变量时,无需任何特殊措施即可安全读取它们。因为屏障在写入周围,所以读取是安全的。 @Henk:Interlocked 调用之后的多个读取不能优化为单个缓存读取吗? 亚当,是的,他们可以。从实时的角度来看,这可能看起来是“错误的”,但不会导致错误的行为。检查联锁成员。只有 64 位值的读取(在 32 位系统上)。 msdn.microsoft.com/en-us/library/… @Henk:如果另一个线程在缓存后更新该值,那怎么会不是不正确的行为? 在这种情况下,变量有可能在Increment 调用之后只被读取一次。除非其他线程在调用Increment 和本次读取之间处理其操作,否则该线程将永远等待,这将导致 other 线程永远等待。【参考方案4】:

我曾经读过关于所谓的非原子和原子(在 VB 中:互锁)操作的非常好的解释,并将尝试总结一下。

Normal "non-atomic" operations consist of several steps 

-> 其他线程可以在这些链之间工作

"Atomic" operations consist of one only one step 

-> 处理原子操作时其他线程无法执行工作,原子操作总是作为整体处理

互锁类是此类原子操作的集合,因此根据定义是线程安全的。 即使多个线程对同一个变量执行读写操作,这些操作也是绝对线程安全的。

这些线程安全命令的组合仍然可能是不安全的,因为在原子操作之间可能会发生竞争条件。

因此,例如,如果您想比较 2 个变量,然后递增较小的变量,这不是线程安全的,即使它们自己的单个操作是 (interlocked.compare, interlocked.increment)。 在这里您仍然必须使用同步锁。

除此限制外,联锁没有“隐藏的不好的一面”。

a = 5 的竞态条件示例:

 Thread1: a+=1
 Thread2: a+=2    
 --> supposed to be 8, but can be only 6 or 7,
 but can also be 6 or 7 depending on which thread wins the race

选项1:

T1 step 1: read 5
T1 step 2: add 1 = 6
T1 step 3: write 6
T2 step 1: read 6
T2 step 2: add 2 = 8
T2 step 3: write 8
--> is 8 as supposed

或选项 2:

T1 step 1: read 5
T2 step 1: read 5
T1 step 2: add 1 = 6
T2 step 2: add 2 = 7
T2 step 3: write 7
T1 step 3: write 6
--> is only 6

或选项 3:

T1 step 1: read 5
T2 step 1: read 5
T1 step 2: add 1 = 6
T2 step 2: add 2 = 7
T1 step 3: write 6
T2 step 3: write 7
--> is only 7

使用 interlocked.increment:

选项 1:

T1 step 1: read 5, add 1, write 6
T2 step 1: read 6, add 2, write 8

或选项 2:

T2 step 1: read 5, add 2, write 7
T1 step 1: read 7, add 1, write 8

-> 在所有情况下 a = 8,线程安全解决方案

这里发布的所有问题都可以通过将这个简单的示例应用于有问题的代码来解决。

希望这可以帮助其他在谷歌上搜索此主题的人。 贾尼斯

【讨论】:

【参考方案5】:

Interlocked 仅限于对 Integer、Long 和 Boolean 等的简单操作。

例如,如果您想将项目添加到共享列表(T)中,您仍然需要SynClock

【讨论】:

确实 - 这是因为它是在硬件级别实现的原子操作,即 CPU 将保证增量不会被争用,因此它需要映射到 CPU 原语,例如 int、longs 等

以上是关于当 Interlocked 类可用时,为啥在 .NET 中使用 SyncLocks 进行简单操作?的主要内容,如果未能解决你的问题,请参考以下文章

Interlocked 是如何工作的,为啥它比 lock 更快? [复制]

C#原子操作(Interlocked.Decrement和Interlocked.Increment)

为啥 Interlocked.CompareExchange<T> 只支持引用类型?

引用分配是原子的,那么为啥需要 Interlocked.Exchange(ref Object, Object)?

使用 .NET 中的 Interlocked 类实现按位运算

当初始化列表可用时,为啥现在使用可变参数?