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 允许多个读操作同时进行,但如果有写操作,它会独占访问。 

 

案例一:

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语言规范的一部分,因此不能保证总是这样。

案例二:对案例一的改进

 

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)
}

 案例三:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/675441.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【设计模式】JAVA Design Patterns——Monitor(监视器模式)

&#x1f50d;目的 主要目的是为多个线程或进程提供一种结构化和受控的方式来安全地访问和操作共享资源&#xff0c;例如变量、数据结构或代码的关键部分&#xff0c;而不会导致冲突或竞争条件。 &#x1f50d;解释 通俗描述 监视器模式用于强制对数据进行单线程访问。 一次只允…

基于java的CRM客户关系管理系统(六)

目录 5.3 表现层设计 5.3.1 模型层&#xff08;M&#xff09; 5.3.2 视图层&#xff08;V&#xff09; 5.3.3 控制层&#xff08;C&#xff09; 5.4 系统主要功能模块的实现 5.4.1 登录功能的实现 5.4.2 客户管理的实现 5.5 本章小结 参考文献 前面内容请移步 基于java…

基本元器件 - 电感与磁珠

电感 电感的选型 体积大小电感值所在工作频率开关频率下的电感值为实际需要的电感值线圈的直流阻抗&#xff08;DCR&#xff09;越小越好工作电流应降额至额定饱和电流的 0.7 倍以下&#xff0c;额定 rms 电流&#xff1b;交流阻抗&#xff08;ESR&#xff09;越小越好&#…

10 款在线剽窃检查的 [免费工具]

剽窃或抄袭他人文章而不注明出处&#xff0c;几乎在所有领域都被认为是有害的。然而&#xff0c;学术界最痛恨这种行为。抄袭是对学术诚信的最大威胁。这就是为什么每个教育机构总是希望学生提交无抄袭的作业。 然而&#xff0c;有时学生无意中剽窃了他人的作业&#xff0c;直…

【软件开发】Java学习路线

本路径视频教程均来自尚硅谷B站视频&#xff0c;Java学习课程我已经收藏在一个文件夹下&#xff0c;B站文件夹同时会收藏其他Java视频&#xff0c;感谢关注。指路&#xff1a;https://www.bilibili.com/medialist/detail/ml3113981545 2024Java学习路线&#xff08;快速版&…

命名空间,缺省参数和函数重载

前言&#xff1a;本文章主要介绍一些C中的小语法。 目录 命名空间 namespace的使用 访问全局变量 namespace可以嵌套 不同文件中定义的同名的命名空间可以合并进一个命名空间&#xff0c;并且其中不可以有同名的变量 C中的输入和输出 缺省参数&#xff08;默认参数&#…

超越Devin!姚班带队,他们创大模型编程新世界纪录

超越Devin&#xff01;SWEBench排行榜上迎来了新玩家—— StarShip CodeGen Agent&#xff0c;姚班带队初创公司OpenCSG出品&#xff0c;以23.67%的成绩获得全球第二名的成绩。 同时创造了非GPT-4o基模的最高纪录&#xff08;SOTA&#xff09;。 我们都知道&#xff0c;SWEBe…

for深入学习

目录 练习&#xff1a; 例1&#xff1a; 求解0-100中整除3的数有哪些 例2&#xff1a; 求0-100中含数字9个个数 作业&#xff1a; 练习&#xff1a; 例1&#xff1a; 求解0-100中整除3的数有哪些 代码&#xff1a; #include<stdio.h> int main() {printf("整…

JAVAEE之网络初识_协议、TCP/IP网络模型、封装、分用

前言 在这一节我们简单介绍一下网络的发展 一、通信网络基础 网络互连的目的是进行网络通信&#xff0c;也即是网络数据传输&#xff0c;更具体一点&#xff0c;是网络主机中的不同进程间&#xff0c;基于网络传输数据。那么&#xff0c;在组建的网络中&#xff0c;如何判断到…

深入理解计算机系统 第三版 中文版 图5-27 p371 错漏

中文版 英文版 对照 可以看出错漏 这本书中文版很多错漏,可以配合英文版查正,不过英文版也很多错漏,所以不用太相信书本.要根据自己的理解来.

TDengine为物联网而生的大数据平台

TDengine为物联网而生的大数据平台 物联网背景 技术支撑 应用落地 未来趋势

【动手学深度学习】softmax回归从零开始实现的研究详情

目录 &#x1f30a;1. 研究目的 &#x1f30a;2. 研究准备 &#x1f30a;3. 研究内容 &#x1f30d;3.1 softmax回归的从零开始实现 &#x1f30d;3.2 基础练习 &#x1f30a;4. 研究体会 &#x1f30a;1. 研究目的 理解softmax回归的原理和基本实现方式&#xff1b;学习…

算法金 | 再见,支持向量机 SVM!

大侠幸会&#xff0c;在下全网同名「算法金」 0 基础转 AI 上岸&#xff0c;多个算法赛 Top 「日更万日&#xff0c;让更多人享受智能乐趣」 一、SVM概述 定义与基本概念 支持向量机&#xff08;SVM&#xff09;是一种监督学习模型&#xff0c;用于解决分类和回归问题。它的核…

Streamsets-JDBC模式offset变化逻辑和如何向下传递offset

Streamsets的版本为3.16.0 离线版 offset在jdbc模式中起到非常关键的作用&#xff0c;是滚动查询的基础&#xff0c;offset的准确直接影响数据同步的质量。 本文主要分享一下JDBC Query Consumer中的offset&#xff0c;包括变化逻辑、存储方式、处理器如何获取到最新的offset。…

如何在QGIS中加载MapBox图源

在设计行业中需要多风格地图的调用&#xff0c;不管是规划、建筑设计还是景观&#xff0c;分析图的工作量都大&#xff0c;有好的底图&#xff0c;会事半功倍。 针对不同项目&#xff0c;会选择不同配色的底图&#xff0c;以便让设计内容中的呈现足够清晰。 这里就来分享一个…

如何在自己的电脑上添加静态路由

1.任务栏搜索powershell 选择以管理员身份运行 2.输入 route add -p (永久) 目的网络地址例如192.168.10.0 mask 255.255.255.0&#xff08;子网掩码&#xff09;192.168.20.1&#xff08;下一跳地址&#xff09;。回车即可生效

238.除以自身以外数组的乘积

给你一个整数数组 nums&#xff0c;返回数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。 题目数据保证数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法&#xff0c;且在 O(n) 时间复杂度内完…

网络编程(六)

网络编程&#xff08;六&#xff09; 广播&组播广播步骤 组播步骤 广播&组播 广播 是一种基于1发送多接收的模型 &#xff08;发送方和接收方&#xff09; 广播是在局域网内实现的&#xff08;发送到广播地址上的消息是会被局域网内同网段的所有主机进行接收&#xf…

[Redis]Set类型

集合类型也是保存多个字符串类型的元素的&#xff0c;但和列表类型不同的是&#xff0c;集合中 1&#xff09;元素之间是无序的 2&#xff09;元素不允许重复 一个集合中最多可以存储2^32-1个元素。 Redis 除了支持集合内的增删查改操作&#xff0c;同时还支持多个集合取交…

深入探讨ChatGPT API中的Tokens计算方式和计算库

引言 在现代人工智能应用中&#xff0c;自然语言处理&#xff08;NLP&#xff09;技术无疑是最受关注的领域之一。OpenAI推出的ChatGPT&#xff0c;作为一种先进的对话模型&#xff0c;已经在多个领域展示了其强大的语言生成能力。为了更好地使用ChatGPT API&#xff0c;理解其…