Go 线程同步

一、介绍

通常在 Go 语言中有两种方法可以用来做线程同步

  1. sync.Cond

  2. channel

channel 的很好理解,当我们从一个 channel 中接收数据的时候,如果里面没有数据,那我们直接就阻塞在那里了。

在 Go 语言中,如果你尝试在已经持有某个锁(例如 sync.Mutexsync.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进入等待状态。当SignalBroadcast被调用,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 运行时的调度器决定。

相关推荐
百流16 分钟前
scala文件编译相关理解
开发语言·学习·scala
Channing Lewis17 分钟前
flask常见问答题
后端·python·flask
Channing Lewis18 分钟前
如何保护 Flask API 的安全性?
后端·python·flask
Evand J1 小时前
matlab绘图——彩色螺旋图
开发语言·matlab·信息可视化
深度混淆2 小时前
C#,入门教程(04)——Visual Studio 2022 数据编程实例:随机数与组合
开发语言·c#
雁于飞2 小时前
c语言贪吃蛇(极简版,基本能玩)
c语言·开发语言·笔记·学习·其他·课程设计·大作业
wenxin-3 小时前
NS3网络模拟器中如何利用Gnuplot工具像MATLAB一样绘制各类图形?
开发语言·matlab·画图·ns3·lr-wpan
数据小爬虫@5 小时前
深入解析:使用 Python 爬虫获取苏宁商品详情
开发语言·爬虫·python
健胃消食片片片片5 小时前
Python爬虫技术:高效数据收集与深度挖掘
开发语言·爬虫·python