一、介绍
通常在 Go 语言中有两种方法可以用来做线程同步
-
sync.Cond
-
channel
channel 的很好理解,当我们从一个 channel 中接收数据的时候,如果里面没有数据,那我们直接就阻塞在那里了。
在 Go 语言中,如果你尝试在已经持有某个锁(例如 sync.Mutex
或 sync.RWMutex
)的情况下再次获取这个锁,这种行为被称为递归锁定 或重入锁定 。Go 的标准库中的 sync.Mutex
是不可递归 的,这意味着你不能在已经持有该锁的 goroutine 中再次调用 Lock()
方法。如果尝试这样做,程序将导致死锁,因为 sync.Mutex
会检测到锁已经被当前 goroutine 持有,并且不会释放它。
例如,以下代码将导致死锁:
如果你需要递归锁定的功能,可以使用 sync.RWMutex
,它允许同一个 goroutine 多次调用 Lock()
和 Unlock()
方法(读锁可以被同一个 goroutine 多次获取,但写锁不能)。sync.RWMutex
允许多个读操作同时进行,但如果有写操作,它会独占访问。
案例一:
Go
package main
import (
"fmt"
"sync"
"time"
)
type SyncNum struct {
num int
lock sync.Mutex //互斥锁(同步锁)
cond *sync.Cond //条件变量
}
func NewSyncNum() *SyncNum {
// 创建 SyncNum 结构体
var obj SyncNum
obj.num = 0
obj.cond = sync.NewCond(&obj.lock) // 使用结构体中的 lock 作为锁
return &obj // 返回该结构体的指针
}
func (num *SyncNum) IncreaseLocked() {
// IncreaseLocked 意味着在做加法操作的时候这个函数需要上锁后才能使用
num.num++
}
func (num *SyncNum) DecreaseLocked() {
// IncreaseLocked 意味着在做减法操作的时候这个函数需要上锁后才能使用
num.num--
}
func (num *SyncNum) Signal() {
// 当完成一件事情后,我们就发送 Signal
num.cond.Signal()
}
func (num *SyncNum) Wait() {
// 当我们调用 Wait 的时候,我们还不能马上执行操作
// 我们需要收到 Signal 后 才可以继续执行
num.cond.Wait()
}
func (num *SyncNum) Lock() {
// 上锁
num.lock.Lock()
}
func (num *SyncNum) UnLock() {
// 解锁
num.lock.Unlock()
}
func main() {
// 设计代码将运算做了加法才能做减法
nu := NewSyncNum()
fmt.Printf("num的初始值为:%d \n", nu.num)
time.Sleep(time.Second)
// 做减法 1000 次
go func(num *SyncNum) {
num.Lock()
fmt.Println("进入了减法并获取了锁,num的值为:", num.num)
time.Sleep(10 * time.Second) //当释放锁后才能获取锁
num.Wait() // 等待信号(调用 Wait 的时候,它会先释放我们传入的那把锁并且阻塞在那里,然后等待信号的到来,当它收到信号之后重新获取那把锁然后再继续执行操作)
fmt.Println("减法并获取了信号,num的值为:", num.num)
for i := 0; i < 1000; i++ {
num.DecreaseLocked()
}
num.Signal() // 发送信号
num.UnLock()
fmt.Println("减法释放锁,num的值为:", num.num)
}(nu)
time.Sleep(time.Second) //这里停顿1秒,是为了先执行减法的协程,然后走到num.Wait()释放锁,阻塞,等待获取信号
// 做加法 1000 次
go func(num *SyncNum) {
fmt.Println("进入了加法,等待获取锁(当释放锁后才能获取锁)")
num.Lock()
fmt.Println("进入了加法并获取了锁,num的值为:", num.num)
for i := 0; i < 1000; i++ {
num.IncreaseLocked()
}
num.Signal() // 发送信号
num.UnLock() // 一定要记得释放锁,不然做减法的 goroutine 那里就永远走不动了
fmt.Println("加法释放锁,num的值为:", num.num)
}(nu)
nu.Lock()
fmt.Println("获取了锁,num的值为:", nu.num)
nu.Wait() //在 sync.Cond 中,等待时间最长的goroutines会被首先唤醒,被唤醒的顺序通常是按照它们被阻塞的顺序(即先进先出,FIFO)
nu.UnLock()
fmt.Printf("释放了锁,num最后的值为:%d \n", nu.num)
}
在Go语言的sync.Cond
中,当调用Signal
方法时,只会唤醒一个等待的goroutine,而当调用Broadcast
方法时,会唤醒所有等待的goroutine。当一个条件变量的Wait
方法被调用时,持有的互斥锁(sync.Mutex
)会被释放,goroutine进入等待状态。当Signal
或Broadcast
被调用,goroutine会被唤醒并尝试重新获取互斥锁。
对于Signal
,只有一个等待的goroutine会被唤醒,而Broadcast
会唤醒所有等待的goroutine。在这两种情况下,被唤醒的goroutine会立即尝试重新获取互斥锁。一旦goroutine成功获取互斥锁,它将再次检查条件(Wait
方法中的条件判断),如果条件仍然不满足,goroutine将再次进入等待状态;如果条件已经满足,goroutine将继续执行。
关于哪个goroutine先拿到信号(即先被唤醒),这取决于Go运行时的调度策略和当前系统的调度情况。在Go中,goroutine的调度是协作式的,并且由Go运行时管理。通常,等待时间最长的goroutine(即FIFO队列中的最前面的goroutine)会被首先唤醒,但这不是Go语言规范的一部分,因此不能保证总是这样。
案例二:对案例一的改进
Go
package main
import (
"fmt"
"sync"
"time"
)
type SyncNum struct {
num int
lock sync.Mutex //互斥锁(同步锁)
cond *sync.Cond //条件变量
}
func NewSyncNum() *SyncNum {
// 创建 SyncNum 结构体
var obj SyncNum
obj.num = 0
obj.cond = sync.NewCond(&obj.lock) // 使用结构体中的 lock 作为锁
return &obj // 返回该结构体的指针
}
func (num *SyncNum) IncreaseLocked() {
// IncreaseLocked 意味着在做加法操作的时候这个函数需要上锁后才能使用
num.num++
}
func (num *SyncNum) DecreaseLocked() {
// IncreaseLocked 意味着在做减法操作的时候这个函数需要上锁后才能使用
num.num--
}
func (num *SyncNum) Signal() {
// 当完成一件事情后,我们就发送 Signal
num.cond.Signal()
}
func (num *SyncNum) Wait() {
// 当我们调用 Wait 的时候,我们还不能马上执行操作
// 我们需要收到 Signal 后 才可以继续执行
num.cond.Wait()
}
func (num *SyncNum) Lock() {
// 上锁
num.lock.Lock()
}
func (num *SyncNum) UnLock() {
// 解锁
num.lock.Unlock()
}
func main() {
// 设计代码将运算做了加法才能做减法
nu := NewSyncNum()
fmt.Printf("num的初始值为:%d \n", nu.num)
time.Sleep(time.Second)
// 做减法 1000 次
go func(num *SyncNum) {
num.Lock()
fmt.Println("进入了减法并获取了锁,num的值为:", num.num)
num.Wait() // 等待信号(调用 Wait 的时候,它会先释放我们传入的那把锁并且阻塞在那里,然后等待信号的到来,当它收到信号之后重新获取那把锁然后再继续执行操作)
fmt.Println("减法并获取了信号,num的值为:", num.num)
for i := 0; i < 1000; i++ {
num.DecreaseLocked()
fmt.Println("-", i)
}
num.UnLock()
fmt.Println("减法释放锁,num的值为:", num.num)
}(nu)
// 做加法 1000 次
go func(num *SyncNum) {
num.Lock()
fmt.Println("进入了加法并获取了锁,num的值为:", num.num)
num.Wait() // 等待信号(调用 Wait 的时候,它会先释放我们传入的那把锁并且阻塞在那里,然后等待信号的到来,当它收到信号之后重新获取那把锁然后再继续执行操作)
fmt.Println("进入了加法并获取了锁,num的值为:", num.num)
for i := 0; i < 1000; i++ {
num.IncreaseLocked()
fmt.Println("+", i)
}
num.UnLock() // 一定要记得释放锁,不然做减法的 goroutine 那里就永远走不动了
fmt.Println("加法释放锁,num的值为:", num.num)
}(nu)
time.Sleep(time.Second) //给goroutines一些时间来进入等待状态
//nu.Lock()
fmt.Println("获取了锁,num的值为:", nu.num)
nu.cond.Broadcast() // 唤醒所有等待的goroutines
//nu.UnLock()
fmt.Printf("释放了锁,num最后的值为:%d \n", nu.num)
time.Sleep(time.Second) //给goroutines一些时间来进入等待状态
fmt.Printf("num最后的值为:%d \n", nu.num)
}
案例三:
Go
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mu sync.Mutex
cond := sync.NewCond(&mu)
var wg sync.WaitGroup
wg.Add(3)
go func() {
mu.Lock()
defer mu.Unlock()
fmt.Println("协程 1 在等待")
cond.Wait() // 释放互斥锁并等待
fmt.Println("协程 1 继续")
wg.Done()
}()
go func() {
mu.Lock()
defer mu.Unlock()
fmt.Println("协程 2 在等待")
cond.Wait() // 释放互斥锁并等待
fmt.Println("协程 2 继续")
wg.Done()
}()
go func() {
mu.Lock()
defer mu.Unlock()
fmt.Println("协程 3 在等待")
cond.Wait() // 释放互斥锁并等待
fmt.Println("协程 3 继续")
wg.Done()
}()
time.Sleep(1 * time.Second) // 模拟一些工作
mu.Lock()
cond.Broadcast() // 唤醒所有等待的goroutines
mu.Unlock()
wg.Wait() // 等待所有goroutines完成
}
3个 goroutine 都会尝试获取互斥锁并调用 cond.Wait()
来阻塞等待。主 goroutine 稍后会调用 cond.Broadcast()
来唤醒所有等待的 goroutine。goroutines 被唤醒的顺序通常是它们被阻塞的顺序,但实际的执行顺序由 Go 运行时的调度器决定。