在多线程环境中,即使是 _serverInfoState
这样的整数变量,直接读取也可能存在可见性问题(不同线程可能看到缓存中的旧值),因此需要通过原子操作确保读取到最新值。
(比喻:关键词:多线程中绝对不能直接操作变量,相当于要包装一层,库存数量变动,银行存款减了多少,全不能直接读写数据库,要包装一个存储过程,还要加事务)
以下是完善后的 VB.NET 模块代码,包含加 1、减 1、加 N、减 N 等原子操作,并规范了安全的读写方式:
多线程安全的VB.NET模块(含原子操作)
Imports System.Threading
Module ServerStateModule
' 模块级变量(默认共享,所有线程可见)
' 注意:公开变量可能导致直接修改,建议私有并通过方法操作
Private _serverInfoState As Integer = 0 ' 0=关闭,1=开启(可扩展为计数)
' 安全判断是否开启(合并版)
Public Function IsServerInfoEnabled() As Boolean
' 直接通过原子操作读取最新值并判断
Return Interlocked.CompareExchange(_serverInfoState, 0, 0) = 1
End Function
' 安全读取当前值(保证可见性)
Public Function GetCurrentState() As Integer
' 用CompareExchange读取最新值(不修改原变量)
Return Interlocked.CompareExchange(_serverInfoState, 0, 0)
End Function
' 安全判断是否开启
Public Function IsServerInfoEnabled2() As Boolean
Return GetCurrentState() = 1
End Function
' 原子设置为开启(1)
Public Sub EnableServerInfo()
Interlocked.Exchange(_serverInfoState, 1)
End Sub
' 原子设置为关闭(0)
Public Sub DisableServerInfo()
Interlocked.Exchange(_serverInfoState, 0)
End Sub
' 原子切换状态(0↔1)
Public Sub ToggleServerInfo()
Do
Dim current = _serverInfoState
Dim newState = If(current = 1, 0, 1)
Loop While Interlocked.CompareExchange(_serverInfoState, newState, current) <> current
End Sub
' 原子加1
Public Function Increment() As Integer
' 返回递增后的值
Return Interlocked.Increment(_serverInfoState)
End Function
' 原子减1
Public Function Decrement() As Integer
' 返回递减后的值
Return Interlocked.Decrement(_serverInfoState)
End Function
' 原子加N(支持正数)
Public Function AddN(value As Integer) As Integer
If value <= 0 Then
Throw New ArgumentException("增加值必须为正数", NameOf(value))
End If
' 循环累加,确保原子性(Interlocked无直接Add方法,需模拟)
Dim initial, updated As Integer
Do
initial = _serverInfoState
updated = initial + value
Loop While Interlocked.CompareExchange(_serverInfoState, updated, initial) <> initial
Return updated
End Function
' 原子减N(支持正数,确保结果不小于0)
Public Function SubtractN(value As Integer) As Integer
If value <= 0 Then
Throw New ArgumentException("减少值必须为正数", NameOf(value))
End If
' 循环递减,确保原子性并防止负数
Dim initial, updated As Integer
Do
initial = _serverInfoState
updated = Math.Max(initial - value, 0) ' 确保不小于0
Loop While Interlocked.CompareExchange(_serverInfoState, updated, initial) <> initial
Return updated
End Function
End Module
关键说明:
-
为什么不能直接读取
_serverInfoState
?- 在多 CPU 环境中,线程可能从各自的缓存中读取值,而非内存中的最新值(缓存一致性问题)。
Interlocked.CompareExchange(_serverInfoState, 0, 0)
会强制刷新缓存,确保读取到内存中的最新值。- 示例中通过
GetCurrentState()
方法统一提供安全读取,避免直接访问变量。
-
原子操作的实现细节:
- 加 1 / 减 1:直接使用
Interlocked.Increment
和Interlocked.Decrement
,这两个方法是原子的且性能最优。 - 加 N / 减 N:.NET 没有直接的原子加减 N 方法,通过
CompareExchange
配合循环实现(类似 CAS 操作),确保整个计算过程的原子性。 - SubtractN 中增加了
Math.Max
保护,防止结果为负数(可根据业务需求调整)。
- 加 1 / 减 1:直接使用
-
变量访问控制:
- 将
_serverInfoState
声明为Private
,避免外部直接修改,强制通过原子方法操作,减少线程安全隐患。 - 提供
GetCurrentState()
作为唯一的读取入口,确保所有线程获取到一致的最新值。
- 将
-
使用场景:
- 既支持简单的开关状态(0/1),也支持计数场景(通过增减操作)。
- 所有方法均为线程安全,可在多用户并发的 IIS 环境中安全使用。
通过这种方式,既能保持模块写法的简洁性,又能严格保证多线程环境下的操作安全性。
原子加N,这样代码太长了,效率行吗?会不会用LOCKOBJ更高效?
原子加 N 的实现代码(基于 CompareExchange
循环)虽然看起来较长,但在低冲突场景下效率通常优于 SyncLock
(lock);而在高冲突场景下,SyncLock
可能更高效(避免大量无效自旋)。两者的取舍需结合实际并发强度判断。
1. 两种方式的效率对比
(1)基于 Interlocked.CompareExchange
的无锁实现(当前方式)vb
' 原子加N(无锁方式)
Public Function AddN(value As Integer) As Integer
If value <= 0 Then Throw New ArgumentException("值必须为正数")
Dim initial, updated As Integer
Do
initial = _serverInfoState
updated = initial + value
Loop While Interlocked.CompareExchange(_serverInfoState, updated, initial) <> initial
Return updated
End Function
- 优势:
无锁操作,避免了SyncLock
的线程阻塞 / 唤醒开销(上下文切换成本)。当并发冲突较少时(多数情况下循环 1-2 次就会成功),效率极高。 - 劣势:
冲突频繁时,会导致循环多次重试(自旋),浪费 CPU 资源;代码相对冗长。
(2)基于 SyncLock
的锁实现
' 定义专用锁对象
Private Shared ReadOnly _lockObj As New Object()
' 原子加N(锁方式)
Public Function AddN(value As Integer) As Integer
If value <= 0 Then Throw New ArgumentException("值必须为正数")
SyncLock _lockObj
_serverInfoState += value
Return _serverInfoState
End SyncLock
End Function
- 优势:
代码简洁,冲突频繁时更稳定(线程会进入等待队列,不消耗 CPU);适合复杂逻辑(如加 N 的同时需要其他操作)。 - 劣势:
锁的获取 / 释放有固定开销(约几十纳秒到微秒级),低冲突场景下效率低于无锁方式;可能导致线程阻塞。
2. 如何选择?
-
优先用无锁(
CompareExchange
循环)的场景:- 并发访问频率不高(冲突少),例如偶尔的计数更新。
- 对延迟敏感(不希望线程阻塞),例如高频次的统计计数。
- 操作逻辑简单(仅加减 N,无额外步骤)。
-
优先用锁(
SyncLock
)的场景:- 并发冲突频繁(多线程同时修改的概率高),例如秒杀场景的库存扣减。
- 操作逻辑复杂(加 N 的同时需要读写其他变量、判断业务规则等)。
- 希望代码更简洁易懂,降低维护成本。
3. 关于 “读取操作” 的补充
无论用哪种方式,多线程中绝对不能直接读取 _serverInfoState
,必须通过安全方式获取最新值:
' 安全读取(无锁方式)
Public Function GetCurrentState() As Integer
Return Interlocked.CompareExchange(_serverInfoState, 0, 0)
End Function
' 或(锁方式,需与修改操作共用同一把锁)
Public Function GetCurrentState() As Integer
SyncLock _lockObj
Return _serverInfoState
End SyncLock
End Function
直接读取可能因 CPU 缓存导致 “脏读”(不同线程看到不同值),两种安全方式需与修改操作的同步机制匹配(无锁改则无锁读,锁改则锁读)。
总结
- 低冲突、简单操作 → 无锁方式(
CompareExchange
循环)更高效。 - 高冲突、复杂操作 → 锁方式(
SyncLock
)更稳定。 - 代码长度不直接决定效率,无锁方式的循环在多数场景下实际执行成本极低。
可根据实际并发压力测试结果选择,多数 Web 场景(中等并发)下,无锁方式是更优选择。