在 Go 中,保护对切片的并发访问有两个方面。
首先,切片是一个连续的内存块,包含零个或多个相同类型的元素;每个都通过其索引访问(0 到 len(slice)-1)。
在 Go 中,切片的每个元素都被视为一个独立变量,这意味着可以同时访问/修改同一切片的不同元素。
重申一下,对于每次这样的访问,只要 N != M 就可以从不同的并发运行的 goroutine 写入同一切片的索引 N 和 M 处的元素。
因此,您只需要将多个 goroutine 执行的并发访问序列化到切片的相同元素。
换句话说,如果您需要从多个同时执行的 goroutine 中读取和/或修改索引 N 处的元素,您需要通过互斥体保护 那个 元素。
现在请注意,在现实生活中,保护单个互斥锁对单个元素的访问的需求很少发生,因此通常只使用单个互斥锁来保护对切片的任何元素的访问。
其次,切片上的某些操作可能会重新分配包含切片元素的内存块,这里有点棘手:比如说,当你这样做时
slice = append(slice, a_new_element)
而slice没有空间容纳a_new_element,会发生两件事:
-
append 将分配一个新的内存块来保存len(slice)+1 元素,然后复制那里的原始内存块的内存。
-
append 将返回一个新的切片描述符,slice 变量的内容将被 = 运算符覆盖。
所有这些操作都会自然地竞争 对涉及索引的切片的任何访问(例如slice[N]):例如,尝试在 goroutine 执行的特定索引处更新切片的元素可能在执行append 的goroutine 将复制切片的内存的同时发生。
由此可见,任何可能重新分配切片内存块的切片操作以及对保存切片描述符的变量的任何更新都必须与正在修改切片元素的所有 goroutine 同步——无论是否这些 goroutine 在修改单个切片的元素方面是同步的。
TL;DR
如果您要从多个 goroutine 访问切片,请使用单个互斥锁来保护对切片的任何种访问。
如果您最终会检测到互斥体将成为瓶颈,那么您可以使用上述逻辑来降低争用。