多设备并发采集时,常见目标是:
同一台设备的采集、连接重建、缓存更新、状态修改必须串行;
不同设备之间可以并发执行。
所以,为每台设备维护一把锁是很自然的做法。但问题不在于“要不要加锁”,而在于:如何保证同一个设备永远拿到同一把锁?
最直观的写法是:
map[string]*sync.Mutex
但普通 map 本身不是并发安全的。两个 goroutine 同时处理同一台设备时,可能同时发现锁不存在,然后各自创建一把锁。结果不仅可能触发 map 并发读写错误,还可能让同一台设备拿到两把不同的锁,互斥关系直接失效。
这个场景更适合用 sync.Map + LoadOrStore:
type DeviceLocks struct {
locks sync.Map // key: guid, value: *sync.Mutex
}
func (d *DeviceLocks) Get(guid string) *sync.Mutex {
actual, _ := d.locks.LoadOrStore(guid, &sync.Mutex{})
return actual.(*sync.Mutex)
}
使用时:
lock := deviceLocks.Get(guid)
lock.Lock()
defer lock.Unlock()
// 执行这台设备的采集、状态更新、缓存写入等操作
这里关键不是 sync.Map 本身,而是 LoadOrStore。
它把“查找”和“创建”合成一个原子操作。即使多个 goroutine 同时获取同一个 guid 的锁,也只有一把锁会真正保存下来,其他临时创建的锁会被丢弃。
不能写成:
if v, ok := d.locks.Load(guid); ok {
return v.(*sync.Mutex)
}
lock := &sync.Mutex{}
d.locks.Store(guid, lock)
return lock
因为 Load 和 Store 是两步操作,中间仍然可能被其他 goroutine 插入,导致同一设备出现多把锁。
不过,sync.Map 只解决了锁表的并发安全,没有解决锁的生命周期问题。
不要轻易删除锁:
d.locks.Delete(guid)
因为可能已有 goroutine 拿到了旧锁,只是还没执行完。此时删除后再创建新锁,就会出现旧 goroutine 用旧锁、新 goroutine 用新锁,同一设备互斥失效。
如果设备规模有限,最简单稳定的做法是:锁常驻。设备下线只更新状态,不删除锁对象。
所以更推荐的结构是:不要单独维护锁,而是维护设备状态对象。
简单场景可以用 sync.Map + LoadOrStore + *sync.Mutex。
工程场景更推荐 sync.Map + LoadOrStore + *DeviceState。