
在 Go 中对共享切片进行并发写入(如 append)是典型的竞态条件场景,即使配合 sync.WaitGroup 等待所有 goroutine 完成,若无同步机制(如 sync.Mutex 或 sync.RWMutex),仍会导致数据丢失、长度异常或 panic。
在 go 中对共享切片进行并发写入(如 `append`)是典型的竞态条件场景,即使配合 `sync.waitgroup` 等待所有 goroutine 完成,若无同步机制(如 `sync.mutex` 或 `sync.rwmutex`),仍会导致数据丢失、长度异常或 panic。
切片([]T)在底层由三部分组成:指向底层数组的指针、长度(len)和容量(cap)。当多个 goroutine 同时调用 append 时,以下竞态行为可能发生:
- 多个 goroutine 同时读取当前
len和cap; - 若
cap不足,append会分配新数组并复制旧数据 —— 此时若两个 goroutine 并发执行该操作,可能各自分配独立新数组,仅其中一个结果被最终赋值给slice变量,另一个完全丢失; - 即使
cap充足,多个 goroutine 也可能并发写入同一底层数组的相邻索引位置,而len的更新(len++)本身非原子操作,导致覆盖或重复写入。
你提供的 WithoutMutex 函数正是典型反例:
func (nonmu *NonMuType) add(i int, wg *sync.WaitGroup) {
nonmu.list = append(nonmu.list, i) // ⚠️ 非线程安全!并发调用时 len/cap 读写竞态
wg.Done()
}
尽管 WaitGroup 确保了主 goroutine 等待全部子 goroutine 退出,但它不保证执行过程中的内存可见性与操作顺序,也无法阻止多个 goroutine 对 nonmu.list 的并发修改。因此,输出长度常小于 10,甚至出现重复或错乱。
相比之下,WithMutex 通过 data.Lock()/Unlock() 强制串行化 append 操作:
func (data *MuType) add(i int, wg *sync.WaitGroup) {
data.Lock() // ✅ 进入临界区:独占访问 list
defer data.Unlock()
data.list = append(data.list, i) // 此时无其他 goroutine 干扰
wg.Done()
}
这确保每次 append 都基于最新状态执行,最终 list 长度恒为 10(符合预期),但元素顺序不确定 —— 因为 goroutine 启动后调度顺序由运行时决定,mutex 仅保“原子性”,不保“顺序性”。
⚠️ 注意事项:
-
sync.RWMutex对写操作(Lock())仍是互斥的,读多写少场景中RLock()可提升读并发性,但写操作必须用Lock(); - 切片本身不是线程安全的数据结构,任何共享可变状态的并发修改都需显式同步;
-
WaitGroup是协作式同步原语,用于等待任务完成,绝不能替代临界区保护; - 若需有序结果(如按
i升序),应避免依赖 goroutine 执行顺序,而改用通道收集或预分配索引+原子写入等方案。
✅ 正确实践建议:
WaitGroup










