Skip to content

Lock

在 Go 中,锁(Lock)用于管理并发访问共享资源,防止多个 goroutine 同时修改资源,确保程序的正确性。Go 提供了几种常见的锁机制,如 互斥锁(Mutex)读写锁(RWMutex),它们位于 sync 包中。

1. 互斥锁(Mutex)

互斥锁(Mutex)是最常见的同步机制之一,保证同一时刻只有一个 goroutine 能访问共享资源。sync.Mutex 提供了两个方法:

  • Lock():用于加锁。
  • Unlock():用于解锁。

1.1 使用 sync.Mutex

下面是一个使用 sync.Mutex 的示例,展示如何保护一个共享变量,以避免并发访问时发生数据竞争:

go
package main

import (
	"fmt"
	"sync"
)

var (
	counter int
	mu      sync.Mutex
)

func increment() {
	mu.Lock()         // 加锁
	defer mu.Unlock() // 解锁

	// 修改共享资源
	counter++
}

func main() {
	var wg sync.WaitGroup

	// 启动多个 goroutine 来并发修改共享资源
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			increment()
		}()
	}

	wg.Wait() // 等待所有 goroutine 完成

	fmt.Println("Counter:", counter) // 输出最终的结果
}

在上面的示例中,mu.Lock()mu.Unlock() 确保每次只有一个 goroutine 可以访问 counter,从而避免了竞态条件。

2. 读写锁(RWMutex)

sync.RWMutex 是另一种常用的锁类型,它允许多个 goroutine 同时读取共享资源,但在写操作时,只允许一个 goroutine 访问资源。这对于读取频繁、写入较少的情况非常有用,可以提高性能。

sync.RWMutex 提供了以下方法:

  • RLock():获取读锁。
  • RUnlock():释放读锁。
  • Lock():获取写锁。
  • Unlock():释放写锁。

2.1 使用 sync.RWMutex

下面是一个示例,展示如何使用 sync.RWMutex 来进行并发读写控制:

go
package main

import (
	"fmt"
	"sync"
)

var (
	data    map[string]string
	mu      sync.RWMutex
)

func readData(key string) string {
	mu.RLock()         // 获取读锁
	defer mu.RUnlock() // 释放读锁
	return data[key]
}

func writeData(key, value string) {
	mu.Lock()         // 获取写锁
	defer mu.Unlock() // 释放写锁
	data[key] = value
}

func main() {
	data = make(map[string]string)

	var wg sync.WaitGroup

	// 启动多个读 goroutine
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			fmt.Println("Reading:", readData("key"))
		}(i)
	}

	// 启动一个写 goroutine
	wg.Add(1)
	go func() {
		defer wg.Done()
		writeData("key", "value")
	}()

	wg.Wait() // 等待所有 goroutine 完成
}

在此示例中,多个 goroutine 可以并发执行读取操作,但在写操作时,mu.Lock() 会阻止任何读写操作,确保在修改数据时没有其他 goroutine 访问该数据。

3. 条件变量(Cond)

sync.Cond 是另一种同步原语,它允许 goroutine 等待某个条件满足时再继续执行。条件变量经常和互斥锁一起使用,以便在某个条件成立时通知其他等待的 goroutine。

sync.Cond 提供了以下方法:

  • Wait():等待通知。
  • Signal():唤醒一个等待的 goroutine。
  • Broadcast():唤醒所有等待的 goroutine。

3.1 使用 sync.Cond

下面是一个使用条件变量的示例:

go
package main

import (
	"fmt"
	"sync"
)

var (
	data  int
	cond  = sync.NewCond(&sync.Mutex{})
)

func produce() {
	// 模拟生产数据
	data = 10
	cond.Signal() // 唤醒等待的消费者
}

func consume() {
	cond.L.Lock() // 获取锁
	defer cond.L.Unlock()

	// 等待直到生产者提供数据
	for data == 0 {
		cond.Wait()
	}

	// 消费数据
	fmt.Println("Consumed:", data)
	data = 0
}

func main() {
	var wg sync.WaitGroup

	// 启动多个消费者
	for i := 0; i < 2; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			consume()
		}()
	}

	// 启动生产者
	wg.Add(1)
	go func() {
		defer wg.Done()
		produce()
	}()

	wg.Wait() // 等待所有 goroutine 完成
}

在这个例子中,消费者会在没有数据时等待,而生产者在生成数据后会唤醒消费者。

4. 原子操作(Atomic)

Go 还提供了一些原子操作,通过 sync/atomic 包来执行一些基本的、不可中断的操作。原子操作不需要显式地加锁,是一种轻量级的同步机制。

常用的原子操作方法包括:

  • atomic.AddInt32()
  • atomic.CompareAndSwapInt32()
  • atomic.LoadInt32()
  • atomic.StoreInt32()

4.1 使用 sync/atomic 实现原子操作

go
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

var counter int32

func increment() {
	atomic.AddInt32(&counter, 1)
}

func main() {
	var wg sync.WaitGroup

	// 启动多个 goroutine 来并发修改共享资源
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			increment()
		}()
	}

	wg.Wait() // 等待所有 goroutine 完成

	fmt.Println("Counter:", counter) // 输出最终的结果
}

在这个示例中,atomic.AddInt32 被用来对 counter 进行原子加法操作,而不需要显式地使用互斥锁。

5. 总结

Go 提供了多种同步机制来确保并发程序的正确性:

  • 互斥锁(Mutex):最常用的锁,保证同一时刻只有一个 goroutine 可以访问共享资源。
  • 读写锁(RWMutex):适用于读多写少的场景,允许多个 goroutine 同时读取共享资源,但写操作时会加锁。
  • 条件变量(Cond):用于在某些条件下同步 goroutine 的执行,适用于生产者消费者问题。
  • 原子操作(Atomic):提供轻量级的原子操作,用于避免加锁的高开销。

根据具体的并发场景,选择合适的锁类型可以有效避免数据竞争并提高程序的性能。