41、Go语言基础 - 并发编程 - 并发同步与锁

作者: 温新

分类: 【Go基础】

阅读: 282

时间: 2023-12-05 00:35:12

hi,我是温新

为什么需要锁?

在 Go 语言中,锁是一种用于并发控制的机制,通常用于保护共享资源,以确保在多个 goroutine同时访问这些资源时不会发生数据竞争的情况。

为什么需要锁,原因如下:

  • 1、**数据竞争:**当多个 goroutine 尝试同时读取或写入相同的共享数据时,可能会发生数据竞争,导致未定义的行为或不确定的结果。锁可以用来同步访问,确保一次只有一个 goroutine 可以修改数据。
  • 2、**保护共享资源:**在多个 goroutine之间共享的数据结构或资源需要受到保护,以防止数据损坏或不一致。锁可以确保在一个 goroutine修改数据时,其他 goroutine不能同时访问它。
  • 3、**同步操作:**有时候,需要在多个 goroutine 之间同步操作,以确保它们按照特定的顺序执行。锁可以用于创建临界区,其中只有一个 goroutine 可以执行特定的代码段,从而实现同步。
  • 4、**防止竞态条件:**竞态条件是指多个 goroutine 之间的操作顺序可能导致不一致或意外的结果。通过使用锁,可以防止竞态条件,确保操作按照期望的方式执行。

锁的实现

在Go语言中,标准库提供了sync包,其中包含了各种锁的实现,包括互斥锁(sync.Mutex)和读写锁(sync.RWMutex),用于管理并发访问。使用锁是一种处理并发问题的通用方式,但需要小心使用,以避免死锁和性能问题。

案例:多个 goroutine竞争数据

package main

import (
	"fmt"
	"sync"
)

// 全局变量
var (
	num int64
	wg  sync.WaitGroup
)

func add() {
	for i := 0; i < 5000; i++ {
		num = num + 1
	}
	wg.Done()
}

func main() {
	wg.Add(2)

	go add()
	go add()

	wg.Wait()
	fmt.Println(num)
}
  1. 定义了一个全局变量 num,它是一个 int 类型的整数,以及一个 sync.WaitGroup 类型的全局变量 wg,用于等待goroutine完成。
  2. 你定义了一个名为 add 的函数。这个函数是用来在循环中增加 num 的值。具体来说,它会循环执行 5000 次,每次将 num 的值增加 1。
  3. main 函数中,你首先调用 wg.Add(2),将 wg 的计数器增加到 2。这是因为将启动两个goroutine,每个都会调用 add 函数。
  4. 接下来,通过 go 关键字并发地启动两个 add 函数的goroutine。这两个goroutine会同时执行 add 函数中的循环,尝试增加 num 的值。
  5. 接着,使用 wg.Wait() 来等待这两个 goroutine 完成。Wait 会阻塞主goroutine,直到 wg 的计数器减为零,表示所有启动的goroutine都已完成。
  6. 最后,你打印 num 的值。由于两个goroutine并发地修改了 num,因此它们可能会相互干扰,导致竞态条件。因此,num 的最终值可能不是你期望的 2 * 5000,因为没有使用锁来保护 num 的并发访问。

互斥锁

在Go语言中,**互斥锁(Mutex)**是一种用于并发控制的同步机制,它用于保护共享资源,以确保在多个goroutine之间的访问是安全的。互斥锁提供了两个主要操作:Lock(加锁)和 Unlock(解锁)。只有一个 goroutine 可以成功获得锁,其他goroutines会被阻塞,直到锁被释放。

互斥锁的定义与方法

互斥锁的定义:

type mutex struct {
    state int32
    sema uint 32
}

互斥锁的方法

方法 功能
func (m *Mutex) Lock() 获取互斥锁
func(m *Mutex) Unlock() 释放互斥锁

Lock() 方法锁住 m,若 m 已加锁,则阻塞直到 m 解锁。

Unlock() 方法解锁 m,若 m 未加锁就会导致运行时错误。

互斥锁案例

下面使用锁来优化上面的案例,得到我们想要的结果,案例如下:

package main

import (
	"fmt"
	"sync"
)

// 全局变量
var (
	num int64
	wg  sync.WaitGroup
	// 互斥锁
	m sync.Mutex
)

func add() {
	for i := 0; i < 5000; i++ {
		// 修改数据前进行加锁
		m.Lock()
		num = num + 1
		// 数据修改完后解锁
		m.Unlock()
	}
	wg.Done()
}

func main() {
	wg.Add(2)

	go add()
	go add()

	wg.Wait()
	fmt.Println(num)
}

如此修改后,无论程序执行多少次,结果都是 10000, 这就是我们想要的结果了。

读写互斥锁

在Go语言中,读写互斥锁(sync.RWMutex)是一种特殊类型的互斥锁,它允许多个 goroutine 同时访问共享资源,只有在写操作时需要排他性,而读操作可以并行执行。这种机制在多读少写的场景中可以提高并发性能。

读写互斥锁的定义与方法

读写互斥锁的定义:

type RWMutex struct {
    w Mutex
    writerSem uint32
    readerSem uint32
    readerCount int32
    readerWait int32
}

互斥锁的方法

方法 功能
func(rw *RWMutex) Lock() 获取写锁
func(rw *RWMutex) Unlock() 释放写锁
func(rw *RWMutex) RLock() 获取读锁
func(rw *RWMutex) RUnlock() 释放读锁
func(rw *RWMutex) RLocker() Locker 返回一个实现 Locker 接口的写锁

读写锁分为两种:读锁和写锁。读写锁的使用中,写操作都是互斥的,读和写是互斥的,读和读不互斥。也就是说,可以同时有多个 goroutine 同时读取数据,但只能有一个 goroutine 写入数据。

Lock() 将 rw 锁定为写入状态,禁止其他 goroutine 读取和写入。

Unlock() 解除 rw 的写入锁,若 rw 未加写锁会导致运行时错误。

RLock() 将 rw 锁定为读取状态,禁止其他 goroutine 写入,但允许读取。

RUnlock() 解除 rw 的读取锁,若 rw 未加读取锁会导致运行时错误。

Rlocker() 返回一个读写锁,通过调用 rw.Rlock()rw.RUnlock() 实现了 Locker 接口。

读写互斥锁案例

package main

import (
	"fmt"
	"sync"
)

var (
	// 共享数据
	sharedValue int
	// 读写互斥锁
	rwMutex sync.RWMutex
)

func main() {
	wg := sync.WaitGroup{}

	// 启动 10 个读取操作的 goroutines
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			rwMutex.RLock() // 获取读锁
			fmt.Printf("Shared value (read): %d\n", sharedValue)
			rwMutex.RUnlock() // 释放读锁
			wg.Done()
		}()
	}

	// 启动 5 个写入操作的 goroutines
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			rwMutex.Lock() // 获取写锁
			sharedValue++  // 增加共享值
			fmt.Printf("Shared value (write): %d\n", sharedValue)
			rwMutex.Unlock() // 释放写锁
			wg.Done()
		}()
	}

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

输出结果:

Shared value (read): 0
Shared value (read): 0
Shared value (read): 0
Shared value (read): 0
Shared value (read): 0
Shared value (read): 0
Shared value (read): 0
Shared value (read): 0
Shared value (read): 0
Shared value (write): 1
Shared value (read): 1
Shared value (write): 2
Shared value (write): 3
Shared value (write): 4
Shared value (write): 5

1、定义了一个整数变量sharedValue,它代表了共享资源,以及一个读写互斥锁rwMutex,用于保护对sharedValue的并发访问;

2、在main函数中,创建了一个sync.WaitGroupwg),用于等待所有的 goroutine 完成;

3、创建 10 个 goroutine,每个都通过rwMutex.RLock()获取读锁,这允许多个goroutine同时并发读取sharedValue的值,并打印出来;

4、创建 5 个 goroutine,每个通过rwMutex.Lock()获取写锁,这确保了写入操作是互斥的。每个写入操作将sharedValue的值增加1,并打印更新后的值,然后通过rwMutex.Unlock()释放写锁。

5、在每个读取和写入操作完成后,它们都调用wg.Done()来通知WaitGroup,表示它们已经完成。

6、最后,通过wg.Wait()阻塞主 goroutine,等待所有启动的 goroutine 完成。这确保了所有读取和写入操作都完成后才打印最终的结果。

条件变量

sync.Cond(条件变量)是 Go 语言标准库中提供的一种同步原语,用于实现 goroutine 之间的协同。条件变量通常与互斥锁(sync.Mutex)结合使用,用于等待和通知的机制。

条件变量的主要操作包括:

  • 1、NewCond(*sync.Mutex) *Cond:创建一个新的条件变量,需要传入一个互斥锁作为参数;
  • 2、c.Wait():等待条件变量,将调用 goroutine 阻塞,直到其他 goroutine 调用c.Signal()c.Broadcast()来通知条件满足;
  • 3、c.Signal():通知等待条件变量的一个 goroutine,使其继续执行。如果没有等待的 goroutine,这个通知会被忽略;
  • 4、c.Broadcast():通知所有等待条件变量的goroutines,使它们继续执行。

条件变量的定义与方法

条件变量的定义:

type Cond struct {
    noCopy noCopy
    L Locker
    notify notifyList
    checker copyChecker
}

条件变量的方法:

func NewCond(l Locker) *Cond:使用 l 创建一个 *Cond。Cond 条件变量总是要和锁结合使用。

func (c *Cond) Broadcase():唤醒所有等待 c 的 goroutine。调用者在调用该方法时,建议保持 c.L 的锁定。

func (c *Cond) Signal():唤醒等待 c 的一个 goroutine。该方法发送通知给一个人。

func (c *Cond) Wait():自行解锁 c.L 并阻塞当前的 goruntine,待线程恢复执行时,Wait() 方法会在返回前锁定 c.L。

条件变量案例

package main

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

func main() {
	// 创建一个互斥锁(mutex)和一个条件变量(cond)
	var mutex sync.Mutex
	cond := sync.Cond{L: &mutex}
	condition := false

	// 启动一个新的goroutine
	go func() {
		// 让子goroutine休眠1秒
		time.Sleep(1 * time.Second)

		// 锁定互斥锁(mutex)
		cond.L.Lock()
		fmt.Println("子goroutine已被锁定")
		fmt.Println("子goroutine更改条件值并发送通知...")
		condition = true

		// 向条件变量发送通知,唤醒等待中的goroutine
		cond.Signal()
		fmt.Println("子goroutine继续...")
		time.Sleep(5 * time.Second)
		fmt.Println("子goroutine解锁")
		// 解锁互斥锁(mutex)
		cond.L.Unlock()
	}()

	// 在主goroutine中锁定互斥锁(mutex)
	cond.L.Lock()
	fmt.Println("main...已被锁定...")

	// 如果条件值为假,则等待条件变量通知
	if !condition {
		fmt.Println("main...即将等待...")
		cond.Wait() // 等待被唤醒
		fmt.Println("main...被唤醒...")
	}

	fmt.Println("main...继续...")
	fmt.Println("main...解锁...")
	// 解锁互斥锁(mutex)
	cond.L.Unlock()
}

输出结果

main...已被锁定...
main...即将等待...
子goroutine已被锁定
子goroutine更改条件值并发送通知...
子goroutine继续...
子goroutine解锁
main...被唤醒...
main...继续...
main...解锁...

代码解释:

1、主函数中创建一个互斥锁(mutex)和一个条件变量(cond);

2、主函数中创建一个布尔变量condition,并初始化为false

3、启动一个子goroutine,该子goroutine会休眠1秒,然后锁定互斥锁(mutex),更改条件变量的值为true,发送通知给等待的 goroutine,再休眠 5 秒,最后解锁互斥锁;

4、主函数中锁定互斥锁(mutex),然后检查条件变量的值,如果条件为false,主 oroutine 将等待条件变量的通知;

5、当子 goroutine 发送通知时,主 goroutine 被唤醒,然后继续执行;

6、最后,主goroutine解锁互斥锁(mutex)。

请登录后再评论