Golang 状态的原子操作
在处理状态的切换处理时为了多线程安全一般会用原子操作实现,这里记录下使用的情况。
1. 在只处理状态量时原子操作对比锁操作
- 只有单个状态量,原子操作比锁更简洁高效
- 使用原子操作也不会影响到其他线程
- 原子操作限制只有一些基本的加减、比较交换,不过针对状态型变量已经够了
2. Golang 原子操作
2.1 CompareAndSwap 方法 ChatGpt 讲解
CompareAndSwap
(CAS)是一个常见的原子操作,常用于实现无锁(lock-free)数据结构。CAS 通过比较当前值和预期值,来决定是否更新某个变量。这个操作通常涉及三个参数:
CompareAndSwap(address, expected, new)
参数解释
address:要操作的内存地址,指向需要修改的值。
expected(old):期望的旧值,表示你“希望”当前值是的值。
new:想要设置的新值。
工作原理
CAS 读取 address 的当前值。
如果当前值与 expected 相等,则将其更新为 new,返回 true(表示成功)。
如果当前值与 expected 不相等,表示在这个操作期间其他线程修改了该值,CAS 操作会失败,返回 false(表示失败)。
示例(伪代码)
// 期望值 old 是 100
// 如果当前值是 100,就更新为 200
if (CompareAndSwap(address, 100, 200)) {
// 更新成功
System.out.println("Value updated!");
} else {
// 更新失败(说明其他线程已修改)
System.out.println("Update failed!");
}
3. 原子实操分析
情景 1 Tcp 连接状态管理
func (t *TcpConn) Close() {
if t.isClosed.CompareAndSwap(false, true) { // CAS 检查并设置关闭状态
t.cancel() // 取消关联的上下文(context)
t.Conn.Close() // 真正关闭 TCP 连接
go t.OnDisconnect.RiseEvent(nil) // 异步触发断开事件
}
}
这里 Close 连接的时候只希望执行关闭操作一次,所以拿 IsClosed 【atomic.bool】作为该状态量,Close 时比对是否连接已经关闭过了(==false),如果没有关闭过(!=false),才会(Swap 该变量到 True)即执行触发条件执行一系列动作。可以设想下多线程执行 Close 方法情况,在 CompareAndSwap 时都会先去比对 false,一个执行成功,其他就不会成功啦!
对比下原写法
func (t *TcpConn) Close() {
isClosed := t.isClosed.Load()
if t.isClosed.CompareAndSwap(isClosed, true) {
//一系列动作
}
}
这里先读取了下 isClosed 状态,然后再次进行 CAS 操作,这个也是线程安全的,对比上面写法可以看出不够简洁
情景 2 设备在线离线状态管理
func (d *Device) SetOnline(value int32) {
old := atomic.SwapInt32(&d.IsOnline, value)
if value != old {
db.sqlUpdateDeviceOnOffline(d.Sn, value)
//进行上线告警
//存储下线位置
...
}
}
这里得到了最新在线离线状态输入到 SetOnline 后进行判断最新在线状态是否和当前状态不一致,即设备切到了上线状态、设备切换到了离线状态,如果发生了状态切换则会进行数据库状态更新、上下线告警等等
在这个场景下使用 isOnline 为【int32】,atomic.SwapInt32
为原子操作修改在线状态为最新状态,得到 old 状态,比对最新状态和 old 状态,如果不等即发生了状态变化
其他写法
- 使用
atomic.LoadInt32
+CompareAndSwapInt32
func (d *Device) SetOnline(value int32) {
old := atomic.LoadInt32(&d.IsOnline) // 用原子操作读取当前值
if atomic.CompareAndSwapInt32(&d.IsOnline, old, value) {
log.Printf("Device state changed: %d -> %d", old, value)
//...........
}
}
这个也是线程安全的
- 错误写法
func (d *Device) SetOnline(value int32) {
if atomic.CompareAndSwapInt32(&d.IsOnline, d.IsOnline, value) {//d.IsOnline为普通读取
// 状态更新成功
}
}
func (d *Device) GetOnline(){
return d.IsOnline //d.IsOnline为普通读取
}
d.IsOnline 读取非原子操作,使用非原子读和原子操作写在并发情况下可能会造成线程安全问题,即
线程 A 开始原子修改 IsOnline
的值。
线程 B 在 SetOnline(1)
执行到一半时,读取 IsOnline
的值。
由于 d.IsOnline
为直接读取而不是通过原子操作,可能读取到部分修改的值或不一致的状态
情景 3 客户端管理
func (n *Hub) AddClient(new *Client) {
logger.Info("hub新增加客户端", zap.String("clientId", new.ClientId))
if pre, ok := n.clients.Swap(new.ClientId, new); ok {
logger.Error(fmt.Sprintf("clientId【%v】已连接,关闭旧连接", new.ClientId))
pre.(*Client).Dispose()
}
new.OnDispose.AddEventListener(func(data interface{}) {
if n.clients.CompareAndDelete(new.ClientId, new) {
logger.Info("hub移除客户端", zap.String("clientId", new.ClientId))
}
})
clients 为【sync.map】
if pre, ok := n.clients.Swap(new.ClientId, new); ok {
这里使用 Swap 操作得到和新 client 冲突的 client,如果存在则进行释放
n.clients.CompareAndDelete(new.ClientId, new)
在触发 Client 释放事件时,这里使用 CompareAndDelete
操作比对存储的 Clients 里是否还是自己,如果是自己则进行移除操作