41、Go语言基础 - 并发编程 - 并发同步与锁
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)
}
- 定义了一个全局变量
num
,它是一个int
类型的整数,以及一个sync.WaitGroup
类型的全局变量wg
,用于等待goroutine完成。 - 你定义了一个名为
add
的函数。这个函数是用来在循环中增加num
的值。具体来说,它会循环执行 5000 次,每次将num
的值增加 1。 - 在
main
函数中,你首先调用wg.Add(2)
,将wg
的计数器增加到 2。这是因为将启动两个goroutine,每个都会调用add
函数。 - 接下来,通过
go
关键字并发地启动两个add
函数的goroutine。这两个goroutine会同时执行add
函数中的循环,尝试增加num
的值。 - 接着,使用
wg.Wait()
来等待这两个 goroutine 完成。Wait
会阻塞主goroutine,直到wg
的计数器减为零,表示所有启动的goroutine都已完成。 - 最后,你打印
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.WaitGroup
(wg
),用于等待所有的 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)。