在 Go 语言中,并发编程是一个重要的特性。并发使得程序可以同时执行多个任务,但也带来了线程间共享数据的安全性问题。为了保证数据的一致性和正确性,需要理解并发安全和锁的概念。

并发安全

并发安全(Concurrency Safety)是指在并发环境下访问共享资源时,能够避免数据竞争(Data Race),确保数据的一致性和正确性。

数据竞争 是指多个 Goroutine 同时访问同一块内存,且至少有一个是写操作时,如果不加以控制,就会导致不可预期的行为,进而产生错误的结果。

并发安全的常见手段

1. 使用互斥锁(Mutex)

Go 提供了 sync.Mutexsync.RWMutex 来实现互斥锁,用于控制对共享资源的访问。

  • sync.Mutex:标准的互斥锁,可以用来锁定和解锁资源,确保同一时刻只有一个 Goroutine 能访问共享资源。

    package main
    
    import (
        "fmt"
        "sync"
    )
    
    var (
        mu      sync.Mutex
        balance int
    )
    
    func Deposit(amount int) {
        mu.Lock()
        balance += amount
        mu.Unlock()
    }
    
    func Balance() int {
        mu.Lock()
        defer mu.Unlock()
        return balance
    }
    
    func main() {
        var wg sync.WaitGroup
    
        for i := 0; i < 10; i++ {
            wg.Add(1)
            go func() {
                defer wg.Done()
                Deposit(100)
            }()
        }
    
        wg.Wait()
        fmt.Println("Final balance:", Balance())
    }
    

    在这个例子中,mu.Lock() 确保在修改 balance 时没有其他 Goroutine 可以访问它,mu.Unlock() 则解锁,使得其他 Goroutine 可以访问。

  • sync.RWMutex:读写互斥锁,允许多个读操作同时进行,但写操作需要独占锁。

    var rwMu sync.RWMutex
    
    func ReadBalance() int {
        rwMu.RLock()    // 加读锁
        defer rwMu.RUnlock()
        return balance
    }
    
    func WriteBalance(amount int) {
        rwMu.Lock()    // 加写锁
        defer rwMu.Unlock()
        balance += amount
    }
    

    在这种情况下,多个 Goroutine 可以同时读取 balance,但写操作需要等待读操作结束后才能进行。

2. 使用 sync.WaitGroup

sync.WaitGroup 用于等待一组 Goroutine 执行完毕。它没有直接与并发安全相关,但通常与锁配合使用,以确保所有 Goroutine 都完成了各自的任务。

3. 使用 sync.Once

sync.Once 用于确保某个操作只执行一次,通常用于初始化操作。

var once sync.Once

func Init() {
    once.Do(func() {
        // 只会执行一次的初始化代码
    })
}

4. 使用通道(Channels)

Go 的通道(Channels)是另一种在并发编程中共享数据的安全方式。它允许 Goroutine 通过通信来共享数据,而不是通过共享内存。

package main

import (
    "fmt"
)

func main() {
    ch := make(chan int)

    go func() {
        ch <- 42 // 发送数据到通道
    }()

    value := <-ch // 从通道接收数据
    fmt.Println(value)
}

通道在设计上避免了数据竞争问题,因为数据在 Goroutine 之间传递时,不存在同时访问共享内存的情况。

总结

  • Mutex 是解决并发安全的直接工具,通过加锁和解锁保护共享资源。
  • RWMutex 提供了读写分离的锁,允许多个读操作并发执行。
  • WaitGroup 用于等待多个 Goroutine 完成,不直接处理并发安全问题,但常与锁结合使用。
  • Channels 提供了一种通过通信而非共享内存来避免并发问题的方法。

正确理解和使用这些工具,是编写并发安全的 Go 代码的关键。