多设备并发采集时,常见目标是:

同一台设备的采集、连接重建、缓存更新、状态修改必须串行;
不同设备之间可以并发执行。

所以,为每台设备维护一把锁是很自然的做法。但问题不在于“要不要加锁”,而在于:如何保证同一个设备永远拿到同一把锁?

最直观的写法是:

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

因为 LoadStore 是两步操作,中间仍然可能被其他 goroutine 插入,导致同一设备出现多把锁。

不过,sync.Map 只解决了锁表的并发安全,没有解决锁的生命周期问题。

不要轻易删除锁:

d.locks.Delete(guid)

因为可能已有 goroutine 拿到了旧锁,只是还没执行完。此时删除后再创建新锁,就会出现旧 goroutine 用旧锁、新 goroutine 用新锁,同一设备互斥失效。

如果设备规模有限,最简单稳定的做法是:锁常驻。设备下线只更新状态,不删除锁对象。

所以更推荐的结构是:不要单独维护锁,而是维护设备状态对象。

简单场景可以用 sync.Map + LoadOrStore + *sync.Mutex

工程场景更推荐 sync.Map + LoadOrStore + *DeviceState