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 运行时的调度器决定。

相关推荐
zmd-zk几秒前
flink学习(2)——wordcount案例
大数据·开发语言·学习·flink
好奇的菜鸟5 分钟前
Go语言中的引用类型:指针与传递机制
开发语言·后端·golang
Alive~o.014 分钟前
Go语言进阶&依赖管理
开发语言·后端·golang
花海少爷16 分钟前
第十章 JavaScript的应用课后习题
开发语言·javascript·ecmascript
手握风云-17 分钟前
数据结构(Java版)第二期:包装类和泛型
java·开发语言·数据结构
许苑向上19 分钟前
Dubbo集成SpringBoot实现远程服务调用
spring boot·后端·dubbo
喵叔哟36 分钟前
重构代码中引入外部方法和引入本地扩展的区别
java·开发语言·重构
尘浮生42 分钟前
Java项目实战II基于微信小程序的电影院买票选座系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
hopetomorrow1 小时前
学习路之PHP--使用GROUP BY 发生错误 SELECT list is not in GROUP BY clause .......... 解决
开发语言·学习·php
郑祎亦1 小时前
Spring Boot 项目 myblog 整理
spring boot·后端·java-ee·maven·mybatis