在Golang的并发编程场景中,多个goroutine同时操作同一个指针指向的共享数据是常见需求,但如果缺乏正确的同步机制,就会引发数据竞争,导致程序出现不可预期的错误。指针本身存储的是内存地址,多个goroutine拿到同一个指针后,会直接操作同一块内存区域,此时如果没有同步控制,读写操作会相互干扰。

指针与并发访问的基础概念
指针是Golang中用于存储变量内存地址的特殊类型,通过指针可以直接修改对应内存地址中的值。而并发安全指的是当多个goroutine同时访问某个资源时,无论访问的顺序如何,程序都能得到正确的结果,不会出现数据损坏或者逻辑错误的情况。
当多个goroutine同时持有同一个指针,并且对该指针指向的数据进行写操作时,就会产生数据竞争。比如两个goroutine同时对一个指针指向的整型变量做加1操作,最终的结果可能比预期的小,因为读和写的操作不是原子的。
未加同步的指针并发访问问题示例
下面是一段没有做任何同步处理的并发访问指针的示例代码,我们可以观察它的运行结果:
package main
import (
"fmt"
"time"
)
// 定义一个存储计数的结构体
type Counter struct {
val int
}
func main() {
// 创建结构体实例,获取指针
c := &Counter{val: 0}
// 启动10个goroutine同时修改指针指向的值
for i := 0; i < 10; i++ {
go func() {
// 每个goroutine对值加1000次
for j := 0; j < 1000; j++ {
c.val++
}
}()
}
// 等待所有goroutine执行完成
time.Sleep(time.Second * 2)
// 打印最终结果,预期是10000
fmt.Println("最终计数结果:", c.val)
}
运行这段代码多次,你会发现最终的结果往往小于10000,这就是因为多个goroutine同时修改c.val时发生了数据竞争,部分加操作被覆盖了。
使用sync.Mutex实现指针并发安全访问
sync包中的Mutex是最常用的互斥锁,它可以保证同一时间只有一个goroutine能够访问被保护的资源,从而实现并发安全。我们可以在结构体内部嵌入Mutex,对指针指向的数据的修改操作加锁。
修改后的安全版本代码如下:
package main
import (
"fmt"
"sync"
"time"
)
// 定义带互斥锁的计数器结构体
type SafeCounter struct {
mu sync.Mutex
val int
}
func main() {
// 获取结构体指针
c := &SafeCounter{val: 0}
// 启动10个goroutine并发修改
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
// 加锁,保证同一时间只有一个goroutine能修改val
c.mu.Lock()
c.val++
// 修改完成后解锁
c.mu.Unlock()
}
}()
}
// 等待所有goroutine完成
wg.Wait()
// 打印结果,此时结果稳定为10000
fmt.Println("最终计数结果:", c.val)
}
这里通过c.mu.Lock()和c.mu.Unlock()把修改val的操作包裹起来,同一时间只有一个goroutine能执行这段逻辑,避免了数据竞争。
使用sync.RWMutex优化读多写少场景
如果我们的场景是读操作远多于写操作,使用Mutex会导致所有读操作也串行执行,效率比较低。这时候可以使用RWMutex,它支持读写锁分离,多个读操作可以同时获取读锁,只有写操作需要获取写锁,写锁和读锁是互斥的。
示例代码如下:
package main
import (
"fmt"
"sync"
"time"
)
// 定义带读写锁的计数器结构体
type RWSafeCounter struct {
mu sync.RWMutex
val int
}
// 读取值的方法
func (c *RWSafeCounter) Get() int {
// 获取读锁,多个goroutine可以同时获取读锁
c.mu.RLock()
defer c.mu.RUnlock()
return c.val
}
// 修改值的方法
func (c *RWSafeCounter) Add(n int) {
// 获取写锁,写锁会阻塞其他读锁和写锁
c.mu.Lock()
defer c.mu.Unlock()
c.val += n
}
func main() {
c := &RWSafeCounter{val: 0}
var wg sync.WaitGroup
// 启动5个写goroutine
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
c.Add(1)
}
}()
}
// 启动10个读goroutine
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
fmt.Println("当前读取值:", c.Get())
}
}()
}
wg.Wait()
fmt.Println("最终计数结果:", c.Get())
}
这种场景下,读操作可以并发执行,只有写操作会阻塞其他操作,整体性能比使用Mutex更高。
其他sync包工具的适用场景
除了Mutex和RWMutex,sync包还提供了其他工具可以配合指针实现并发安全:
- sync.WaitGroup:用于等待一组goroutine执行完成,避免主goroutine提前退出,上面的示例中已经用到。
- sync.Once:保证某个操作在并发场景下只执行一次,比如指针指向的资源的初始化操作,适合只需要执行一次的初始化逻辑。
- sync.Map:如果指针指向的是map类型的数据,直接使用map加锁也可以,但sync.Map是官方提供的并发安全的map实现,更适合并发场景下的map操作。
注意事项
在使用指针和sync包实现并发安全时,需要注意以下几点:
- 锁的粒度要合适,不要过大也不要过小,过大会影响并发性能,过小可能导致部分逻辑没有被保护到。
- 不要忘记解锁,最好使用defer语句来解锁,避免程序出现异常时锁没有被释放,导致死锁。
- 不要对同一个指针指向的数据混用不同的同步机制,比如一部分操作加Mutex,另一部分操作加RWMutex,会导致同步失效。
- 指针传递时要注意,不要将未加同步的指针随意传递给多个goroutine,尽量把同步逻辑封装在结构体内部,对外提供安全的方法。
通过合理结合指针和sync包的工具,我们可以在Golang的并发场景中安全地操作共享数据,避免数据竞争带来的问题,保证程序的正确性和稳定性。