Go并发安全基础 | 青训营

49 阅读2分钟

1. 使用 Mutex 实现并发安全

在 Go 语言中,可以使用 sync.Mutex 来设置锁,从而保证临界区内只有一个协程能够访问。在给定的示例代码中,通过声明一个名为 locksync.Mutex 对象来实现并发安全。

var lock = sync.Mutex{}
var n int32

func foo() {
	for i := 0; i < 100000; i++ {
		lock.Lock()  // 加锁
		n++
		lock.Unlock()  // 解锁
	}
	fmt.Println("n=", n)
}

func main() {
	wg := sync.WaitGroup{}
	wg.Add(2)
	for i := 0; i < 2; i++ {
		go func() {
			defer wg.Done()
			foo()
		}()
	}
	wg.Wait()
	fmt.Println("MAIN n=", n)
}

上述代码展示了如何使用 sync.Mutex 来保护共享变量 n 的原子性操作。在 foo 函数中,我们使用 lock.Lock() 来获取锁以进入临界区,然后对 n 进行加一操作,并通过 lock.Unlock() 来释放锁。这样就确保了任意时刻只有一个协程能够修改 n 的值,避免了数据竞争。

2. 使用 atomic 实现并发安全

除了使用 sync.Mutex 进行加锁和解锁,Go 语言还提供了 atomic 包,其中的原子操作函数可以用于实现并发安全。

var n int32

func foo() {
	for i := 0; i < 100000; i++ {
		atomic.AddInt32(&n, 1)  // 原子加一操作
	}
	fmt.Println("n=", n)
}

func main() {
	wg := sync.WaitGroup{}
	wg.Add(2)
	for i := 0; i < 2; i++ {
		go func() {
			defer wg.Done()
			foo()
		}()
	}
	wg.Wait()
	fmt.Println("MAIN n=", n)
}

在上述示例代码中,我们使用了 atomic.AddInt32() 函数来对共享变量 n 进行原子加一操作。这个函数能够保证该操作是原子性的,从而避免了并发情况下的数据竞争。通过将 &n 传递给 atomic.AddInt32() 函数,我们确保了对 n 的访问是原子的。

需要注意的是,相对于使用 sync.Mutex 进行加锁和解锁的方式,使用 atomic 包进行原子操作可以更加高效。因为原子操作通常在硬件级别上实现,不需要像锁一样引入系统调用和上下文切换。

无论是使用 Mutex 还是 atomic 包,都可以保证并发程序的安全。但在性能方面,如果只是对一个简单的变量进行原子操作,使用 atomic 包可能更加高效。而对于更复杂的临界区,使用 Mutex 可以更好地控制访问权限,但相应地可能会引入更多的开销。

同时,需要注意的是,并发安全只是保证了数据的一致性和正确性,但并不能解决所有并发问题。在实际开发中,我们还需要考虑到竞态条件、死锁等问题,以及如何提高并发程序的性能等方面的挑战。