云原生探索系列(十六):Go 语言锁机制

前言

在并发编程中,锁是保证数据一致性、避免竞争条件的重要工具。假如有多个线程连续向同一个缓存区写入数据,如果没有一个 机制协调这些线程的写入操作的话,被写入的数据块就可能出现错乱,线程A还没有写完,线程B就开始写入,这样就会造成数据 混乱。那该如何解决呢?

Go 语言提供了 sync.Mutexsync.RWMutex ,用于实现互斥访问共享资源的功能。 这篇文章就来探讨 sync.Mutexsync.RWMutex的使用方式及区别。

1. sync.Mutex :互斥锁

sync.Mutex 用于保护共享资源,确保同一时刻只有一个 goroutine 能够访问被锁定的代码段。

基本用法

Mutex 提供了两种主要方法:

  • Lock() :加锁,若锁已被其他 goroutine 获取,则会阻塞直到该锁被释放。
  • Unlock() :解锁,释放该锁,使其他等待的 goroutine 能够获取锁。

示例

先来看看,不加锁的代码示例:

go 复制代码
var counter int

func increment() {
	counter++
}

func main() {
	var wg sync.WaitGroup

	// 启动 100 个 goroutine,每个 goroutine 都会执行 increment 函数
	for i := 0; i < 100; i++ {
	    wg.Add(1)
		go func() {
		    defer wg.Done()
			increment()
		}()
	}

	wg.Wait()
	fmt.Println("Counter:", counter)
}

这段代码启动了 100 个并发的 goroutine,每个 goroutine 执行 increment 函数来增加 counter 的值。 代码的目的是希望通过并发的方式多次执行 increment ,最终输出 counter 的值。

  • 全局变量 countercounter 是一个全局整数变量,所有的 goroutine 都会访问和修改这个变量。
  • increment 函数: 该函数的作用是对 counter 进行自增操作,使用 counter++ 来增加其值。
  • sync.WaitGroupsync.WaitGroup 用来等待所有 goroutine 完成。 wg.Wait() 会阻塞主 goroutine,直到所有被 Add 方法注册的 goroutine 都执行完毕。
  • 启动 100 个 goroutine: 在 main 函数中,使用一个 for 循环启动 100 个 goroutine。每个 goroutine 都会执行 increment 函数,该函数的目的是将 counter 增加 1。

这段代码启动了 100 个 goroutine,期望最终输出的 counter 值是 100。但是你运行代码,发现有时候并不是100,这是出啥问题了呢?

  • 竞态条件(Race Condition): 由于多个 goroutine 并发地修改 counter 变量, counter++ 并不是原子操作,它由三部分组成:

    • 读取 counter 的当前值
    • 将其加 1
    • 将新的值写回 counter

    在并发的情况下,如果多个 goroutine 同时执行这三个步骤,它们可能会读取相同的值、加 1 后写回,导致丢失某些增量,最终导致 counter 的值少于预期的 100。

    例如:

    • 假设 counter 当前是 0。
    • 两个 goroutine 同时读取到 counter 的值 0,并分别加 1。然后,它们都会写回值 1。
    • 结果是 counter 只增加了 1,而不是 2。

下面我们改正代码,加锁,解决竞态问题:

go 复制代码
var counter int
var mu sync.Mutex // 使用互斥锁保护 counter

func increment() {
	mu.Lock()         // 加锁,防止多个 goroutine 同时修改 counter
	counter++
	defer mu.Unlock() // 解锁,确保其他 goroutine 可以访问 counter
}

func main() {
	var wg sync.WaitGroup

	// 启动 100 个 goroutine,每个 goroutine 都会执行 increment 函数
	for i := 0; i < 100; i++ {
		wg.Add(1) // 增加一个待等待的 goroutine
		go func() {
			defer wg.Done() // 完成时调用 Done,减少计数
			increment()
		}()
	}

	wg.Wait() // 等待所有 goroutine 执行完
	fmt.Println("Counter:", counter)
}

这段代码,我们在 increment 函数中使用了sync.Mutex

  • sync.Mutex 锁的使用: 在 increment 函数中,使用了一个 sync.Mutex 锁来确保每次只有一个 goroutine 修改 counter 变量,从而避免了并发冲突和竞态条件。
  • mu.Lock() 用于加锁
  • defer mu.Unlock() 确保在 increment 函数执行完成后释放锁。

再次执行代码,最终输出的 counter 值将是 100。

适用场景:

当多个 goroutine 需要对共享资源进行读写操作时,如果每次只有一个 goroutine 可以访问该资源, Mutex 是最适合的选择。

2. sync.RWMutex :读写锁

sync.RWMutex 是一个比 sync.Mutex 更为细粒度的锁,它允许多个读操作并行进行,但写操作是互斥的。 换句话说,多个读操作可以共享锁,而写操作会阻塞所有读操作和其他写操作。

基本用法

sync.RWMutex 提供了以下方法:

  • RLock() :获取读锁,允许多个 goroutine 并行获取读锁,但如果有写锁被占用,则阻塞。
  • RUnlock() :释放读锁。
  • Lock() :获取写锁,阻塞所有读锁和写锁。
  • Unlock() :释放写锁。

示例

我们有一个计数器,多个 goroutine 会不断地增加计数器的值。同时,我们也会有一些 goroutine 来读取计数器的当前值。为了提高性能,允许多个 goroutine 并发读取,但写操作则必须是独占的。

go 复制代码
// Counter 使用读写锁来实现线程安全的计数器
type Counter struct {
	mu    sync.RWMutex
	count int
}

// 增加计数器的值
func (c *Counter) Increment() {
	c.mu.Lock() // 获取写锁,阻塞其他读写操作
	defer c.mu.Unlock()
	c.count++
}

// 读取计数器的值
func (c *Counter) GetValue() int {
	c.mu.RLock() // 获取读锁,允许多个 goroutine 并发读取
	defer c.mu.RUnlock()
	return c.count
}

func main() {
	var counter Counter
	var wg sync.WaitGroup

    // 启动 5 个 goroutine 来增加计数器的值
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			fmt.Printf("Goroutine %d: Incrementing counter\n", id)
			counter.Increment()
		}(i)
	}

	// 启动 10 个 goroutine 来读取计数器的值
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			fmt.Printf("Goroutine %d: Current counter value is %d\n", id, counter.GetValue())
		}(i)
	}

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

	// 输出最终的计数器值
	fmt.Printf("Final counter value: %d\n", counter.GetValue())
}

这段代码中:

  • Counter 结构体: count 用来存储计数器的值。 mu 是一个 sync.RWMutex 类型的读写锁,保证了对 count 的并发读写操作是线程安全的。
  • Increment 方法: 获取写锁 c.mu.Lock() ,这意味着在执行增量操作时,其他的读操作和写操作都会被阻塞,直到当前操作完成。 完成后释放锁:defer c.mu.Unlock()。
  • GetValue 方法: 获取读锁 c.mu.RLock() ,允许多个 goroutine 并发读取计数器的值,但在获取读锁期间无法进行写操作。 完成后释放读锁: defer c.mu.RUnlock()
  • main 函数: 创建了多个 goroutine 来读取计数器的值,并且启动了多个 goroutine 来增加计数器的值。 使用 sync.WaitGroup 来等待所有 goroutine 执行完成。

适用场景:

如果共享资源需要频繁读取且不常写入,使用 RWMutex 可以提高并发性能。多个 goroutine 可以并行读取数据,而只有在需要写操作时才会阻塞

最后

互斥锁和读写锁整个使用起来还是比较简单的,但想要用的好,还得结合业务场景,多思考。

相关推荐
程序媛学姐1 分钟前
SpringBoot Actuator指标收集:Micrometer与Prometheus集成
spring boot·后端·prometheus
欲儿11 分钟前
RabbitMQ原理及代码示例
java·spring boot·后端·rabbitmq
林 子12 分钟前
Spring Boot自动装配原理(源码详细剖析!)
spring boot·后端
编程轨迹13 分钟前
使用 Spring 和 Redis 创建处理敏感数据的服务
后端
未完结小说1 小时前
服务注册与发现(nacos)
后端
AI智能科技用户7946329781 小时前
okcc呼叫中心两个sip对接线路外呼任务怎么设置才能一个任务对应yigesip中继?
人工智能·后端
风舞雪凌月1 小时前
【安全】DVWA靶场渗透
安全·web安全·云原生·eureka
懒虫虫~1 小时前
Spring源码中关于抽象方法且是个空实现这样设计的思考
java·后端·spring
Paraverse平行云1 小时前
如何使用UE Cesium插件实现网页端无算力负担访问?
云原生·webrtc
雷渊1 小时前
DDD的分层架构是怎么样的?
后端