Go中(goroutine)同步机制原子函数和互斥锁

152 阅读1分钟

Go 语言提供了传统的同步 goroutine 的机制,就是对共享资源加锁。如果需要顺序访问一个 整型变量或者一段代码,atomicmutexsync包提供的解决方案

原子函数

原子函数能够以很底层的加锁机制来同步访问整型变量和指针

什么是原子操作?

原子操作是一种不可被中断的操作,它可以在多线程或分布式系统中保证数据的一致性。一个原子操作要么完全执行成功并且不会受到任何外部因素的干扰,要么就是完全不执行。

原子操作通常用于对共享资源进行读取和修改的场景,例如加锁、解锁、更新计数器等。

在多线程或分布式系统中多个线程或节点可能同时访问同一个共享资源,如果没有采取措施来保证原子性,那么就有可能出现数据不一致的情况。使用原子操作可以避免这种情况发生,并确保所有的线程或节点都能够正确地访问和修改共享资源。

atomic

以下是一个使用 atomic 包进行原子操作的示例代码:

package main

import (
    "fmt"
    "sync/atomic"
)

func main() {
    var count int32 = 0

    // 并发执行 10000 次自增操作
    for i := 0; i < 10000; i++ {
        go func() {
            atomic.AddInt32(&count, 1)
        }()
    }

    // 等待所有 Goroutine 执行完毕
    for atomic.LoadInt32(&count) < 10000 {}

    // 输出计数器结果
    fmt.Println("count =", count)
}

Counter: 1000

在上面的代码中,我们定义了一个名为 count 的变量,并将其初始化为 0。接着,我们并发执行了 10000 次自增操作,每次通过 atomic.AddInt32 函数对 count 进行原子加 1 操作。最后,在主 Goroutine 中等待计数器达到 10000 后输出结果。

使用 atomic 包能够确保在多个 Goroutine 同时访问 count 变量时仍然能够保持数据一致性,并且避免了出现竞态条件导致程序错误的问题

在 Go 语言中,atomic 包提供了一组原子操作函数,用于对内存地址的读取、比较和交换等操作进行原子化处理。这些操作可以在多个 Goroutine 之间安全地共享变量或状态。


atomic 包中最常用的几个函数包括:

  • atomic.LoadInt32(addr *int32) int32:以原子方式读取指定内存地址上的 int32 值。
  • atomic.StoreInt32(addr *int32, val int32):以原子方式将指定值 val 写入到内存地址 addr 中。
  • atomic.AddInt32(addr *int32, delta int32) int32:以原子方式将指定值 delta 加到内存地址 addr 中,并返回新值。
  • atomic.CompareAndSwapInt32(addr *int32, old, new int32) bool:比较内存地址 addr 上的 int32 值与旧值 old 是否相等,若相等则将新值 new 写入内存地址 addr 中,并返回 true,否则不作任何修改并返回 false。
  • ... 还有其他不同位数的方法,此处省略

这些函数都是原子的,可以保证在多个 Goroutine 并发访问时不会出现数据竞争问题,从而确保程序的正确性和稳定性。


mutex

mutex是一种同步原语,用于实现对共享资源的互斥访问。mutex是互斥锁的简写,意味着在任何给定时间只能有一个线程访问被保护的代码块。

当多个Go协程需要同时访问某个共享资源时,如果不进行同步控制,就会导致数据竞争问题,从而出现不可预测、不正确的结果。使用mutex可以解决这个问题,它可以确保同一时间只有一个协程可以访问被保护的代码块,从而避免了数据竞争问题。

使用sync包提供的Mutex类型来创建和使用mutex。例如,可以通过以下方式声明和初始化一个mutex

var mu sync.Mutex

package main

import (
	"fmt"
	"sync"
)

var (
	counter = 0
	mu      sync.Mutex
)

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			mu.Lock()
			defer mu.Unlock()
			counter++
			wg.Done()
		}()
	}
	wg.Wait()
	fmt.Println("Counter:", counter)
}

Counter: 1000

mu.Lock() 获取互斥锁,防止其他 goroutine 进入随后的临界区(critical section)代码,直到执行 mu.Unlock() 这确保了在任何给定时间只有一个 goroutine 能够访问共享资源,从而避免竞态条件和数据损坏。

以下是每行代码的简要解释:

  • mu.Lock():获取互斥锁。
  • 临界区代码:需要互斥/isolation 的代码部分。
  • mu.Unlock():释放互斥锁。

参考书籍《Go语言实战》