Go基础学习09-多协程资源竞争、sync.Mutex、sync.Cond、chan在多协程对共享变量的资源竞争中的使用

文章目录

  • Go中协程基础小记
    • 协程基础
    • 为什么需要多协程
    • 多协程面临的问题
  • 代码演示
    • 存在共享变量的资源竞争
    • Mutex解决资源竞争
      • Mutex基本介绍
      • 代码演示
    • Mutex+Cond解决资源竞争和CPU空转
      • Cond条件变量讲解
      • 代码演示
      • 代码片段关于wait和for循环的解释说明
      • 条件变量Signal方法和Broadcast方法有那些异同
    • chan解决资源竞争

Go中协程基础小记

协程基础

在Go中,线程被程为goroutine,也有人称其为线程的线程(a thread of thread)。每个线程都有自己的单独的变量如:

PC程序计数器、函数调用栈Stack、一套寄存器Registers。

在Go中协程对应的primitive原语有:

  • start/go:启动/运行一个线程
  • exit:线程退出,一般从某个函数退出/结束执行后,会自动隐式退出
  • stop:停止一个线程,比如向一个没有读者的channel写数据,那么channel阻塞,go可能会运行时暂时停止这个线程
  • resume:恢复原本停止的线程重新执行,需要恢复程序计数器(program counter)、栈指针(stack pointer)、寄存器(register)状态,让处理器继续运行该线程。

为什么需要多协程

依靠多协程达到并发的效果:

  • I/O concurrency:I/O并发。
  • multi-core parallelism:多核并行,提高整体吞吐量。充分利用CPU。
  • convinience:方便,经常有需要异步执行or定时执行的任务,可以通过线程完成。

多协程面临的问题

  • race conditions:多线程会引入竞态条件的场景
    • avoid sharing:避免共享内存以防止竞态条件场景的产生(Go有一个竞态检测器race detector,能够辅助识别代码中的一些竞态条件场景)
    • use locks:让一系列指令变成原子操作
  • coordination:同步协调问题,比如一个线程的执行依赖另一个线程的执行结果等
    • channels:通道允许同时通信和协调
    • condition variables:配合互斥锁使用
  • deadlock:死锁问题,比如在go中简单的死锁场景,一个写线程往channel写数据,但是永远没有读线程从channel读数据,那么写线程被永久阻塞,即死锁,go会抓住这种场景,抛出运行时错误runtime error。

go中还存在一个协程泄漏的问题:参考下面代码示例第四个:channel的使用。

代码演示

存在共享变量的资源竞争

// 定义一个工具类,不具有实际意义。
package mainimport "math/rand"func requestVote() bool {intn := rand.Intn(10)if intn <= 5 {return true}return false
}// 基础代码
package mainfunc main() {count := 0finished := 0for i := 0; i < 10; i++ {// 匿名函数,创建共 10 个线程go func() {vote := requestVote() // 一个内部sleep随机时间,最后返回true的函数,模拟投票if vote {count++}finished++}()}for count < 5 && finished != 10 {// wait}if count >= 5 {println("received 5+ votes!")} else {println("lost")}
}

使用go自带的资源竞争分析工具:

go run -race xxx.go

执行结果如下:

==================
WARNING: DATA RACE
Write at 0x00c00001c118 by goroutine 9:main.main.func1()/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:20 +0x77Previous read at 0x00c00001c118 by main goroutine:main.main()/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:25 +0x147Goroutine 9 (running) created at:main.main()/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:17 +0x71
==================
==================
WARNING: DATA RACE
Write at 0x00c00001c128 by goroutine 9:main.main.func1()/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:22 +0xa4Previous read at 0x00c00001c128 by main goroutine:main.main()/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:25 +0x164Goroutine 9 (running) created at:main.main()/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:17 +0x71
==================
received 5+ votes!
==================
WARNING: DATA RACE
Read at 0x00c00001c128 by goroutine 12:main.main.func1()/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:22 +0x91Previous write at 0x00c00001c128 by goroutine 11:main.main.func1()/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:22 +0xa4Goroutine 12 (running) created at:main.main()/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:17 +0x71Goroutine 11 (finished) created at:main.main()/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:17 +0x71
==================
Found 3 data race(s)
exit status 66

通过上述代码运行结果,可以得知存在多个协程的情况下对共享变量count、finished存在资源竞争。

Mutex解决资源竞争

Mutex基本介绍

互斥锁Mutex可以看作是针对某一个临机区或一组相关临界区的唯一访问令牌。
互斥锁的使用需要主要以下要求:

  1. 不要重复锁定互斥锁;
  2. 不要忘记解锁互斥锁,必要时使用defer语句;
  3. 不要对尚未锁定或者已解锁的互斥锁解锁;
  4. 不要在多个函数之间直接传递互斥锁。

代码演示

代码示例:

package mainimport ("sync"
)func main() {count := 0finished := 0var mu sync.Mutexfor i := 0; i < 10; i++ {// 匿名函数,创建共 10 个线程go func() {vote := requestVote() // 一个内部sleep随机时间,最后返回true的函数,模拟投票// 临界区加锁mu.Lock()// 推迟到基本block结束后执行,这里即函数执行结束后 自动执行解锁操作。利用defer语言,一般在声明加锁后,立即defer声明推迟解锁defer mu.Unlock()if vote {count++}finished++}()}for {mu.Lock()if count > 5 || finished == 10 {// 不能在此处解锁,下面仍然需要对count变量进行判断//mu.Unlock()break}mu.Unlock()// wait}if count >= 5 {println("received 5+ votes!")} else {println("lost")}mu.Unlock()
}

运行结果:

wt@wt:~/Backend/go/goprojects/src/golearndetail/concurrency/learn01$ go run -race voteselect_mutex.go utils.go 
received 5+ votes!

使用Mutex后,及时加锁以及解锁,不存在资源竞争。

Mutex+Cond解决资源竞争和CPU空转

Cond条件变量讲解

条件变量是基于互斥锁的,它必须有互斥锁的支撑才能发挥作用。同时需要注意条件变量不是被用来保护临界区和共享资源的,它是用于协调想要访问共享资源的那些线程的。当共享资源的状态发生变化时,它可以被用来通知被互斥锁阻塞的线程。

如何初始化一个条件变量:

	var mu sync.Mutexcond := sync.NewCond(&mu)

上图使用锁mu的指针类型初始化一个cond条件变量:条件变量的初始化离不开互斥锁,并且条件变量的所有方法也是基于互斥锁的。

条件变量提供的方法有哪些:

  • 等待通知(wait):等待通知的使用需要基于条件变量相关联的互斥锁加锁的保护下使用。
  • 单发通知(signal):
  • 广播通知(broadcast):对于单发通知或者广播通知的使用与等待通知的使用恰恰相反,需要在对应的互斥锁解锁之后再使用。

代码演示

代码示例:

package mainimport "sync"func main() {count := 0finished := 0var mu sync.Mutex//cond := sync.NewCond(&mu)cond := sync.Cond{L: &mu}for i := 0; i < 10; i++ {// 匿名函数,创建共 10 个线程go func() {vote := requestVote() // 一个内部sleep随机时间,最后返回true的函数,模拟投票// 临界区加锁mu.Lock()// 这里只有一个waiter,所以用Signal或者Broadcast都可以defer cond.Broadcast()// 推迟到基本block结束后执行,这里即函数执行结束后 自动执行解锁操作。利用defer语言,一般在声明加锁后,立即defer声明推迟解锁defer mu.Unlock()if vote {count++}finished++// Broadcast 唤醒所有等待条件变量 c 的 goroutine,无需锁保护;Signal 只唤醒任意 1 个等待条件变量 c 的 goroutine,无需锁保护。}()}mu.Lock()for count < 5 || finished != 10 {// 如果条件不满足,则在制定的条件变量上wait。内部原子地进入sleep状态,并释放与条件变量关联的锁。当条件变量得到满足时,这里内部重新获取到条件变量关联的锁,函数返回。cond.Wait()// 使用cond.Wait的目的防止CPU空转,使用time.sleep()无法控制合适的休眠时间}if count >= 5 {println("received 5+ votes!")} else {println("lost")}mu.Unlock()
}

执行结果:

wt@wt:~/Backend/go/goprojects/src/golearndetail/concurrency/learn01$ go run -race voteselect_mutex_cond.go utils.go 
received 5+ votes!

代码片段关于wait和for循环的解释说明

观察下面代码片段:

	for count < 5 || finished != 10 {// 如果条件不满足,则在制定的条件变量上wait。内部原子地进入sleep状态,并释放与条件变量关联的锁。当条件变量得到满足时,这里内部重新获取到条件变量关联的锁,函数返回。cond.Wait()// 使用cond.Wait的目的防止CPU空转,使用time.sleep()无法控制合适的休眠时间}
// -----------------------------------------------------------------------// 临界区加锁mu.Lock()// Broadcast 唤醒所有等待条件变量 c 的 goroutine,无需锁保护;Signal 只唤醒任意 1 个等待条件变量 c 的 goroutine,无需锁保护。// 这里只有一个waiter,所以用Signal或者Broadcast都可以defer cond.Broadcast()// 推迟到基本block结束后执行,这里即函数执行结束后 自动执行解锁操作。利用defer语言,一般在声明加锁后,立即defer声明推迟解锁defer mu.Unlock()

上述代码中的条件变量cond调用Wait()方法时做了哪些?同时为什么需要使用for循环进行状态判断而不是if条件进行状态判断?

条件变量Wait方法主要做了四件事:

  1. 把调用它的 goroutine(也就是当前的 goroutine)加入到当前条件变量的通知队列中。
  2. 解锁当前的条件变量基于的那个互斥锁:所以调用Wait()方法之前必须将条件变量关联的锁进行上锁,如果没有锁定互斥锁,在执行解锁操作时会触发一个panic。
  3. 让当前的 goroutine 处于等待状态,等到通知到来时再决定是否唤醒它。此时,这个goroutine 就会阻塞在调用这个Wait方法的那行代码上。
  4. 如果通知到来并且决定唤醒这个 goroutine,那么就在唤醒它之后重新锁定当前条件变量基于的互斥锁。自此之后,当前的 goroutine 就会继续执行后面的代码了。

调用Wait()方法后能否不解锁当前互斥锁?
不可以:如果当前goroutine不解锁当前互斥锁,依靠其他goroutine来解锁当前互斥锁,可能发生多次解锁,引发panic。所以不能通过其他goroutine来解锁当前互斥锁,只能当前goroutine来解锁。如果当前goroutine不解锁对应互斥锁,那么没有任何goroutine能够进入临界区,并改变共享资源的状态。程序所有goroutine全部阻塞。

为什么要使用for循环对条件变量进行判断,而不是使用if对条件变量进行判断?
if语句只会对共享资源的状态检查一次,而for语句却可以做多次检查,直到这个状态改变为止。 例如:如果一个 goroutine 因收到通知而被唤醒,但却发现共享资源的状态,依然不符合它的要求,那么就应该再次调用条件变量的Wait方法,并继续等待下次通知的到来。(上面代码演示的就是这个效果,如果使用If第一次唤醒后不满足条件就直接退出,无法进行后续的判断。)

条件变量Signal方法和Broadcast方法有那些异同

相同点:

  • Signal方法和Broadcast方法都是被用来发送通知的,

不同点:

  • Signal方法只会唤醒一个因此而等待的 goroutine,而Broadcast的通知却会唤醒所有为此等待的 goroutine。
  • 条件变量的Wait方法总会把当前的 goroutine 添加到通知队列的队尾,而它的Signal方法总会从通知队列的队首开始查找可被唤醒的 goroutine。所以,因Signal方法的通知而被唤醒的goroutine 一般都是最早等待的那一个。

Signal方法和Broadcast方法使用注意事项:

  1. 条件变量的Signal方法和Broadcast方法并不需要在互斥锁的保护下执行。恰恰相反,我们最好在解锁条件变量基于的那个互斥锁之后,再去调用它的这两个方法。这更有利于程序的运行效率。(上述代码展示)
  2. 条件变量的通知具有即时性。也就是说,如果发送通知的时候没有 goroutine为此等待,那么该通知就会被直接丢弃。在这之后才开始等待的 goroutine 只可能被后面的通知唤醒。

chan解决资源竞争

代码示例:

package mainfunc main() {count := 0finished := 0ch := make(chan bool)for i := 0; i < 10; i++ {// 匿名函数,创建共 10 个线程go func() {ch <- requestVote()}()}// 这里实现并不完美,如果count >= 5了,主线程不会再监听channel,导致其他还在运行的子线程会阻塞在往channel写数据的步骤。// 但是这里主线程退出后子线程们也会被销毁,影响不大。但如果是在一个长期运行的大型工程中,这里就存在泄露线程leaking threadsfor count < 5 && finished != 10 {// 主线程在这里等待v := <-chif v {count++}finished++}if count >= 5 {println("received 5+ votes!")} else {println("lost")}
}

上述代码中涉及到一个线程泄漏的问题:

在Go语言中,"泄露线程leaking threads"通常指的是goroutine泄漏。Goroutine是Go语言中实现并发的轻量级线程,它们应该是临时的和短暂的。然而,如果一个goroutine因为某些原因无法正常结束,它就会一直运行,从而导致资源无法释放,这就是所谓的goroutine泄漏。
上面代码如果长期运行,由于非缓冲通道没有数据接收者,会导致部分goroutine一直阻塞在向其写数据的操作中,造成线程泄漏。

对于非缓冲通道的理解可以参考我的另一篇博文:
https://blog.csdn.net/weixin_45863010/article/details/142618550?spm=1001.2014.3001.5501
执行结果:

wt@wt:~/Backend/go/goprojects/src/golearndetail/concurrency/learn01$ go run -race voteselect_channel.go utils.go 
received 5+ votes!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++七种异常处理

在C++中,使用异常机制可以提高程序的健壮性和可维护性。异常是在程序运行时发生的一个事件,它会打断正在执行的程序的正常流程。C++异常处理机制可以使程序在出现异常时,进行异常处理,而不是退出程序。 基本的异常处理 #include <iostream> using namespace std;int …

IP 数据包分包组包

为什么要分包 由于数据链路层MTU的限制,对于较⼤的IP数据包要进⾏分包. 什么是MTU MTU相当于发快递时对包裹尺⼨的限制.这个限制是不同的数据链路对应的物理层,产⽣的限制. • 以太⽹帧中的数据⻓度规定最⼩46字节,最⼤1500字节,ARP数据包的⻓度不够46字节,要在后⾯补填 充…

云栖实录 | 开源大数据全面升级:Native 核心引擎、Serverless 化、湖仓架构引领云上大数据发展

本文根据2024云栖大会实录整理而成&#xff0c;演讲信息如下&#xff1a; 演讲人&#xff1a; 王 峰 | 阿里云智能集团研究员、开源大数据平台负责人 李 钰&#xff5c;阿里云智能集团资深技术专家 范 振&#xff5c;阿里云智能集团高级技术专家 李劲松&#xff5c;阿里云…

[论文笔记] LLaMA3.2

https://github.com/meta-llama/llama-stack [2407.21783] The Llama 3 Herd of Models Meta 部落格資訊:https://ai.meta.com/blog/llama-3-2-connect-2024-vision-edge-mobile-devices/HuggingFace:

【Kubernetes】常见面试题汇总(五十)

目录 112.考虑一个公司要向具有各种环境的客户提供所有必需的分发产品的方案。您如何看待他们如何动态地实现这一关键目标&#xff1f; 113.假设一家公司希望在从裸机到公共云的不同云基础架构上运行各种工作负载。在存在不同接口的情况下&#xff0c;公司将如何实现这一目标&…

MongoDB聚合操作及索引底层原理

目录 链接:https://note.youdao.com/ynoteshare/index.html?id=50fdb657a9b06950fa255a82555b44a6&type=note&_time=1727951783296 本节课的内容: 聚合操作: 聚合管道操作: ​编辑 $match 进行文档筛选 ​编辑 将筛选和投影结合使用: ​编辑 多条件匹配: …

Solidity智能合约中的事件和日志

1. Solidity 中的事件和日志概述 1.1 什么是事件&#xff1f; 在 Solidity 中&#xff0c;事件&#xff08;Event&#xff09;是一种允许智能合约与外部世界进行通信的机制。通过触发事件&#xff0c;可以记录合约执行中的关键操作&#xff0c;并将这些操作发送到链上。事件的…

魔改xjar支持springboot3,

jar包加密方案xjar, 不支持springboot3。这个发个魔改文章希望大家支持 最近公司需要将项目部署在第三方服务器&#xff0c;于是就有了jar包加密的需求&#xff0c;了解了下目前加密方案现况如下: 混淆方案&#xff0c;就是在代码中添加大量伪代码&#xff0c;以便隐藏业务代…

常见电脑品牌BIOS设置与进入启动项快捷键

常见电脑品牌BIOS与引导项快捷键速查表 | 电脑品牌 | BIOS快捷键 | 引导项快捷键 | 备注 ||------------|------------|--------------|------------------------------ || 联想 | F2/F1 | F12 | 笔记本通常为F2&#xff0c;台式机通常为F1 || IBM/ThinkPad | F1 | 未知 | ||…

【AI】AIOT简介

随着技术的快速发展&#xff0c;人工智能AI和物联网IoT已经成为当今最热门的技术领域。AIOT是人工智能和物联网的结合&#xff0c;使物联网设备更加智能化&#xff0c;能够进行自主决策和学习的技术。 通过物联网产生、收集来自不同维度的、海量的数据存储于云端、边缘端&#…

Ubuntu上安装Git:简单步骤指南

Git是目前世界上最流行的版本控制系统&#xff0c;广泛用于软件开发中。无论你是开发者还是版本控制的新手&#xff0c;Git都是你不可或缺的工具。本文将为你介绍如何在Ubuntu操作系统上安装Git。 什么是Git&#xff1f; Git是一个开源的分布式版本控制系统&#xff0c;由Lin…

数据治理006-数据标准的管理

元数据的分类和标准有哪些&#xff1f; 一、元数据的分类 元数据可以根据其描述的对象和属性不同&#xff0c;被分为不同的类型。以下是几种常见的元数据分类方法&#xff1a; 基于数据的类型&#xff1a;根据数据的类型&#xff0c;元数据可以被分为结构化元数据、非结构化元…

SQL连接Python

对于运营部门的Yoyo来说&#xff0c;她想要知道夜曲优选的订单都来自哪些省份&#xff0c;每个省份的总订单数以及总订单金额分别是多少。 这时小鹿就会通过SQL对连接的数据库进行查询&#xff0c;再将结果传递给Python处理&#xff0c;并帮助Yoyo生成可视化图表。 我们先来快…

拆解维修飞科剃须刀

原因 用了好几年的剃须刀&#xff0c;经过一次更换电池。后来上面的盖帽松动&#xff0c;无法合盖&#xff0c;经过把弹片矫正后修复。最近一次”大力出奇迹“的操作直接断送了这个老伤员最后的可能性。最终只能花了将近十块大洋买了一套盖着和中间座。简单更换了一下。 记录…

单元测试进阶-Mock使用和插桩

目录 一、基本概念 1、Mock 2、插桩&#xff08;Sutbbing&#xff09; 二、参考文章 一、基本概念 1、Mock Mock的作用就是不直接new对象&#xff0c;而是使用Mock方法或者注解Mock一个对象。 这个对象他不是new创建的对象&#xff0c;Mock对该对象的一些成员变量和方法…

【RabbitMQ 项目】服务端:消费者管理模块

文章目录 一.概念辨析1.什么是消费者&#xff1f;2.服务端为什么要管理消费者&#xff1f;3.怎么管理消费者&#xff1f;4.需要管理生产者吗&#xff1f; 二.编写思路1.定义消费者2.定义队列消费者管理管理类3.定义消费者管理类 三.代码实践 一.概念辨析 1.什么是消费者&#…

html嵌入百度地图

html嵌入百度地图 key地址 https://lbsyun.baidu.com/apiconsole/key#/home &#xff0c;点进去注册应用、然后复制key换掉即可显示地图 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>百度地图搜索…

C++随心记 续一

C中的模板 在其它语言中如Java或者C#中可能叫做泛型&#xff0c;在C中为模板&#xff0c;泛型的限制通常比模板多。模板可以解决多次的代码重复问题&#xff0c;如以下场景 #include <iostream> #include <string>void print(int value) {std::cout << val…

MySQL存储和处理XML数据

文章目录 一、实战概述二、准备数据三、实战步骤1、创建数据库2、创建数据表3、插入XML数据4、查询XML数据5、修改XML数据6、删除XML数据7、注意事项四、实战小结一、实战概述 MySQL不直接支持XML数据类型,但可以通过TEXT或BLOB类型字段存储XML数据,利用ExtractValue和Update…

Spark Job 对象 详解

在 Apache Spark 中&#xff0c;Job 对象是执行逻辑的核心组件之一&#xff0c;它代表了对一系列数据操作&#xff08;如 transformations 和 actions&#xff09;的提交。理解 Job 的本质和它在 Spark 中的运行机制&#xff0c;有助于深入理解 Spark 的任务调度、执行模型和容…