Go语言并发编程 ------ 锁机制详解

发布于:2025-08-18 ⋅ 阅读:(15) ⋅ 点赞:(0)

Go语言提供了丰富的同步原语来处理并发编程中的共享资源访问问题。其中最基础也最常用的就是互斥锁(Mutex)和读写锁(RWMutex)。

1. sync.Mutex(互斥锁)

Mutex核心特性

  • 互斥性/排他性同一时刻只有一个goroutine能持有锁
  • 不可重入:同一个goroutine重复加锁会导致死锁
  • 零值可用sync.Mutex的零值就是未锁定的互斥锁
  • 非公平锁:不保证goroutine获取锁的顺序

Mutex例子

例1:

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

var wait sync.WaitGroup
var count = 0

var lock sync.Mutex

func main() {
	wait.Add(10)
	for i := 0; i < 10; i++ {
		go func(data *int) {
			// 加锁
			lock.Lock()
			// 模拟访问耗时
			time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
			// 访问数据
			temp := *data
			// 模拟计算耗时
			time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
			ans := 1
			// 修改数据
			*data = temp + ans
			// 解锁
			lock.Unlock()
			fmt.Println(*data)
			wait.Done()
		}(&count)
	}
	wait.Wait()
	fmt.Println("最终结果", count)
}

输出:

1
2
3
4
5
6
7
8
9
10
最终结果 10

解读:

  • lock 是一个互斥锁,用于确保在任何时刻只有一个 goroutine 可以访问和修改 count 变量,防止数据竞争。
  • 每个 goroutine 首先通过 lock.Lock() 加锁,确保在同一时间只有一个 goroutine 可以修改 count。
  • time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000))) 模拟了对数据的访问和计算耗时,这里的随机数生成器用于在每次循环中生成一个 0 到 999 之间的随机整数,作为睡眠的时间
  • wait.Wait() 阻塞主 goroutine,直到等待组中的所有 goroutine 都完成任务。
  • fmt.Println("最终结果", count) 打印 count 的最终值。

在 Go 语言中,func(data *int) 这样的写法是用来定义一个匿名函数,并且该匿名函数接受一个参数,参数类型是指向整型的指针。在这段代码的目的是在并发环境中对一个共享变量 count 进行修改,以避免数据竞争。

  • go func(data *int) { ... }(&count) 这里的 go 关键字用于启动一个新的 goroutine。
  • func(data *int) { ... } 是一个匿名函数,它接受一个参数 data,这个参数是一个指向整型的指针。
  • (&count) 表示传递给匿名函数的参数是 count 变量的地址。通过传递指针,匿名函数可以直接访问和修改 count 的值。

例2

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	counter int
	lock    sync.Mutex
)

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go increment(&wg)
	}
	wg.Wait()
	fmt.Println("Final counter:", counter)
}

func increment(wg *sync.WaitGroup) {
	defer wg.Done()
	
	lock.Lock()         // 加锁
	defer lock.Unlock() // 使用defer确保解锁
	
	// 临界区
	temp := counter
	time.Sleep(1 * time.Millisecond)
	counter = temp + 1
}

输出:

Final counter: 10

解读:

  • var wg sync.WaitGroup:声明了一个等待组wg,用于等待所有goroutine完成。
  • wg.Add(1):为每次循环增加一个等待计数。
  • go increment(&wg):启动goroutine运行increment函数,并传入等待组的地址。
  • wg.Wait():等待所有等待计数为零,即所有goroutine完成。
  • defer wg.Done():使用defer关键字确保函数执行完毕后调用wg.Done(),减少等待组的一个计数。
  • lock.Lock():在函数执行前加锁,防止多个goroutine同时访问counter。
  • defer lock.Unlock():同样使用defer关键字确保函数执行完毕后解锁。
  • 临界区代码段:将counter的值赋给temp,休眠1毫秒,然后将counter设置为temp + 1。这里通过休眠模拟了一个耗时操作。

2. sync.RWMutex(读写锁)

Go 中读写互斥锁的实现是 sync.RWMutex,它也同样实现了 Locker 接口,但它提供了更多可用的方法,如下:

// 加读锁
func (rw *RWMutex) RLock()

// 非阻塞地尝试加读锁 (Go 1.18+)
func (rw *RWMutex) TryRLock() bool

// 解读锁
func (rw *RWMutex) RUnlock()

// 加写锁
func (rw *RWMutex) Lock()

// 非阻塞地尝试加写锁 (Go 1.18+)
func (rw *RWMutex) TryLock() bool

// 解写锁
func (rw *RWMutex) Unlock()

1. RWMutex基本概念

读写锁的特点

  • 并发读:多个goroutine可以同时持有读锁
  • 互斥写:写锁是排他的,同一时间只能有一个goroutine持有写锁
  • 写优先:当有写锁等待时,新的读锁请求会被阻塞,防止写锁饥饿

Mutex的区别

特性 Mutex RWMutex
并发读 不支持 支持多个goroutine同时读
并发写 不支持 不支持
性能 一般 读多写少场景性能更好
复杂度 简单 相对复杂

2. RWMutex的工作原理

锁状态

  • 当写锁被持有时:所有读锁和写锁请求都会被阻塞
  • 当读锁被持有时:新的读锁请求可以立即获得锁,写锁请求会被阻塞
  • 当写锁请求等待时:新的读锁请求会被阻塞(写优先)

内部实现要点

  1. 读者计数:记录当前持有读锁的goroutine数量
  2. 写者标记:标识是否有goroutine持有或等待写锁
  3. 写者信号量:用于唤醒等待的写者
  4. 读者信号量:用于唤醒等待的读者

3. RWMutex的例子

线程安全的缓存实现

type Cache struct {
    mu    sync.RWMutex
    items map[string]interface{}
}

func (c *Cache) Get(key string) (interface{}, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    item, found := c.items[key]
    return item, found
}

func (c *Cache) Set(key string, value interface{}) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    c.items[key] = value
}

func (c *Cache) Delete(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    delete(c.items, key)
}

解读

Cache 结构体

  • items:一个映射(map),键为字符串,值为接口类型(interface{}),用于存储缓存数据。
  • mu:一个sync.RWMutex实例,用于控制对items的并发访问。

Get 方法:

  • c.mu.RLock():获取读锁,允许多个读协程同时访问items。
  • defer c.mu.RUnlock():确保在函数返回前释放读锁
  • item, found := c.items[key]:从items中获取指定key对应的值,并判断该key是否存在。
  • return item, found:返回获取的值和是否找到的布尔值。

Set 方法:

  • c.mu.Lock():获取写锁,确保只有一个写协程可以访问items。
  • defer c.mu.Unlock():确保在函数返回前释放写锁
  • c.items[key] = value:将指定key对应的值设置为value。

Delete 方法:

  • c.mu.Lock():获取写锁,确保只有一个写协程可以访问items。
  • defer c.mu.Unlock():确保在函数返回前释放写锁。
  • delete(c.items, key):从items中删除指定key对应的键值对。

3.互斥锁和读写锁的区别和应用场景

核心区别对比

特性 互斥锁(Mutex) 读写锁(RWMutex)
并发读 完全互斥,读操作也需要独占锁 允许多个goroutine同时持有读锁
并发写 互斥,同一时间只有一个写操作 互斥,同一时间只有一个写操作
锁类型 单一锁类型 区分读锁(RLock)和写锁(Lock)
性能开销 较高(所有操作都互斥) 读操作开销低,写操作开销与Mutex相当
实现复杂度 简单 相对复杂
适用场景 读写操作频率相当或写多读少 读操作远多于写操作的场景

选择场景

  1. 优先考虑RWMutex当

    • 读操作次数是写操作的5倍以上
    • 读操作临界区较大(耗时较长)
    • 需要支持高频并发读取
  2. 选择Mutex当

    • 读写操作频率相当(写操作占比超过20%)
    • 临界区非常小(几个CPU周期就能完成)
    • 代码简单性比极致性能更重要
    • 需要锁升级/降级(虽然Go不支持,但Mutex更不容易出错)
  3. 特殊考虑

    • 对于极高性能场景,可考虑atomic原子操作
    • 对于复杂场景,可考虑sync.Map或分片锁

网站公告

今日签到

点亮在社区的每一天
去签到