【Go 快速入门】协程 | 通道 | select 多路复用 | sync 包

文章目录

  • 前言
    • 协程
      • goroutine 调度
      • 使用 goroutine
    • 通道
      • 无缓冲通道
      • 有缓冲通道
      • 单向通道
    • select 多路复用
    • sync
      • sync.WaitGroup
      • sync.Mutex
      • sync.RWMutex
      • sync.Once
      • sync.Map

项目代码地址:05-GoroutineChannelSync

前言

Go 1.22 版本于不久前推出,更新的新特性可以参考官文。从此篇章开始,后续 go 版本更为 1.22.0 及以上,自行官网下载。

协程

常见的并发模型

  • 线程与锁模型
  • Actor 模型
  • CSP 模型
  • Fork 与 Join 模型

Go 语言天生支持并发,主要通过基于通信顺序过程(Communicating Sequential Processes, CSP)的 goroutine 和通道 channel 实现,同时也支持传统的多线程共享内存的并发方式。

goroutine 会以一个很小的栈开始其生命周期,一般只需要 2 KB。goroutine 由 Go 运行时(runtime)调度,Go 运行时会智能地将 m 个 goroutine 合理的分配给 n 个操作系统线程,实现类似 m:n 的调度机制,不再需要开发者在代码层面维护线程池。

goroutine 调度

操作系统线程的调度:操作系统线程在被内核调度时挂起当前执行的线程,并将它的寄存器内容保存到内存中,然后选出下一次要执行的线程,并从内存中恢复该线程的寄存器信息,恢复现场并执行该线程,这样就完成一次完整的线程上下文切换。

goroutine 调度:区别于操作系统线程的调度,goroutine 调度在 Go 语言运行时层面实现,完全由 Go 语言本身实现,按照一定规则将所有的 goroutine 调度到操作系统线程上执行。

goroutine 调度器采用 GPM 调度模型,如下所示:

在这里插入图片描述

  • G:表示 goroutine,每执行一次go f()就创建一个 G,包含要执行的函数和上下文信息。

  • 全局队列(Global Queue):存放等待运行的 G。

  • P:表示 goroutine 执行所需的资源,最多有 GOMAXPROCS 个。GOMAXPROCS 默认 CPU 核心数,指定需要使用多少个操作系统线程来同时执行代码。

  • P 的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建 G 时,G 优先加入到 P 的本地队列,如果本地队列满了会批量移动部分 G 到全局队列。

  • M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,当 P 的本地队列为空时,M 也会尝试从全局队列或其他 P 的本地队列获取 G。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。

  • Goroutine 调度器和操作系统调度器是通过 M 结合起来的,每个 M 都代表了1个内核线程,操作系统调度器负责把内核线程分配到 CPU 的核上执行。

参考:https://www.liwenzhou.com/posts/Go/concurrence/


使用 goroutine

启动 goroutine 只需要在函数前加 go 关键字:

func f(msg string) {for i := 0; i < 3; i++ {fmt.Println(msg, ":", i)}
}func functino01() {go f("goroutine")go func(msg string) {fmt.Println(msg)}("going")time.Sleep(time.Second)fmt.Println("done")
}
going
goroutine : 0
goroutine : 1
goroutine : 2
done

使用 time.Sleep 等待协程 goroutine 的运行不优雅,同时也不够精确,后续会采用 sync 包提供的常用并发原语,对协程的运行状态进行控制。

在 go 1.22.0 版本后,如下使用可正常在协程闭包函数中捕获外部的变量,而不是每个 loop 仅一份变量了。

参考:https://zhuanlan.zhihu.com/p/674158675

func function05() {for i := 0; i < 5; i++ {go func() {fmt.Println(i)}() // 正常输出 0~4 中的数字,而不是全是 4}time.Sleep(time.Second)
}

通道

通道 channel 是一种特殊类型,遵循先入先出(FIFO)的特性,用于 goroutine 之间的同步、通信。

声明 channel 语法如下:

chan T 		// 双向通道
chan <- T  	// 只能发送的通道
<- chan T	// 只能接收的通道

channel 是一个引用类型,在被初始化前值为 nil,需要使用 make 函数进行初始化。缓冲区大小可选:

  • 有缓冲通道:make(chan T, capacity int)
  • 无缓冲通道:make(chan T)make(chan T, 0)

通道共有三种操作,发送、接受、关闭:

  • 定义通道
ch := make(chan int)
  • 发送一个值到通道中
ch <- 10
  • 从通道中接收值
v := <- ch 		// 从 ch 接收值赋给 v
v, ok := <- ch 	// 多返回值,ok 表示通道是否被关闭
<- ch			// 从 ch 接收值,忽略结果
  • 关闭通道
close(ch)

tips

  • 对一个关闭的通道发送值会导致 panic
  • 对一个关闭的通道一直获取值会直到通道为空
  • 重复关闭通道会 panic
  • 通道值可以被垃圾回收
  • 对一个关闭并且没值的通道接收值,会获取对应类型零值

无缓冲通道

又称阻塞通道,同步通道。

无缓冲通道必须至少有一个接收方才能发送成功,即发送操作会阻塞,直到另一个 goroutine 在该通道上接收。相反,接收操作先执行,也会阻塞至有 goroutine 往通道发送数据。

发送方和接收方要同步就绪,只有在两者都 ready 的情况下,数据才能在两者间传输。

等待一秒后,主程才能获取到 ch 中的数据

func function02() {ch := make(chan int, 0)go func() {time.Sleep(time.Second)ch <- 1}()v := <-chfmt.Println(v)
}

等待一秒后,协程中才能获取到 ch 中的数据

func function03() {ch := make(chan int)go func() {v := <-chfmt.Println(v)}()time.Sleep(time.Second)ch <- 1time.Sleep(time.Second)
}

有缓冲通道

又称异步通道

有缓冲通道可以通过 cap 获取通道容量,len 获取通道内元素数量。如果通道元素数量达到上限,那么继续往通道发送数据也会被阻塞,直至有 goroutine 从通道获取数据。

通常选择使用 for range 循环从通道中接收值,当通道被关闭后,通道内所有值被接收完毕后会自动退出循环。

func function04() {ch := make(chan int, 2)fmt.Println(len(ch), cap(ch)) // 0 2ch <- 1ch <- 2go func() {for v := range ch {fmt.Println(v)}}() // 1 2 3ch <- 3time.Sleep(time.Second)
}
  • 多返回值模式

基本格式:value, ok := <- ch

ok :如果为 false 表示 value 为无效值(通道关闭后的默认零值);如果为 true 表示 value 为通道中的实际数据值。

func function06() {ch := make(chan int, 1)ch <- 1close(ch)go func() {for {if v, ok := <-ch; ok {fmt.Println(v)} else {break}}}()time.Sleep(time.Second)
}

单向通道

通常会在函数参数中限制通道只能用于接收或发送。控制通道在函数中只读或只写,提升程序的类型安全。

// Producer 生产者
func Producer() <-chan int {ch := make(chan int, 1)go func() {for i := 0; i < 3; i++ {ch <- i}close(ch) // 任务完成关闭通道}()return ch
}// Consumer 消费者
func Consumer(ch <-chan int) int {sum := 0for v := range ch {sum += v}return sum
}func function07() {ch := Producer()sum := Consumer(ch)fmt.Println(sum) // 3
}

在函数传参及赋值过程中,全向通道可以转为单向通道,但单向通道不可转为全向通道。

func function08() {ch := make(chan int, 1)go func(ch chan<- int) {for i := 0; i < 2; i++ {ch <- i}close(ch)}(ch)for v := range ch {fmt.Println(v)} // 0 1
}

Go 语言采用的并发模型是 CSP,提倡通过通信实现内存共享,而不是通过共享内存实现通信。

CSP 模型由并发执行的实体所组成,实体之间通过发送消息进行通信。

Go 通过 channel 实现 CSP 通信模型,主要用于 goroutine 之间的消息传递和事件通知。


select 多路复用

在从多个通道获取数据的场景下, 需要使用 select 选择器,使用方式类似于 switch 语句,有一系列的 case 分支和一个默认分支。

基本格式:

select {
case <- ch1:...
case data := <- ch2:...
case ch3 <- 3:...
default:...
}

select 会一直等待,直到其中某个 case 的通信操作完成,执行该 case 语句。

  • 可处理一个或多个 channel 的接收和发送
  • 如果多个 case 同时满足,select 随机选择一个执行
func function09() {now := time.Now()ch1 := make(chan string)ch2 := make(chan string)go func() {time.Sleep(1 * time.Second)ch1 <- "one"}()go func() {time.Sleep(2 * time.Second)ch2 <- "two"}()for i := 0; i < 2; i++ {select {case msg1 := <-ch1:fmt.Println(msg1)case msg2 := <-ch2:fmt.Println(msg2)}} // one twofmt.Println(time.Since(now)) // 2.0003655s
}

sync

在上述示例中,使用了大量的 time.Sleep 等待 goroutine 的结束。但还有更好的方式,使用内置的 sync 包管理协程的运行状态。

sync.WaitGroup

使用 wait group 等待多个协程完成,如果 WaitGroup 计数器恢复为 0,即所有协程的工作都完成:

var (x  int64wg sync.WaitGroup
)func function10() {add := func() {defer wg.Done()for i := 0; i < 5000; i++ {x = x + 1}}wg.Add(2)go add()go add()wg.Wait()fmt.Println(x)
}

使用 go run -race main.go 可查看代码是否存在竞态问题,上述代码存在两个 goroutine 操作同一个资源,输出结果不定。

方法作用
WaitGroup.Add(delta)计数器值 +delta,建议在 goroutine 外部累加计数器
WaitGroup.Done()计数器值 -1
WaitGroup.Wait()阻塞代码,直到计数器值减为 0

注意:WaitGroup 对象不是一个引用类型,在通过函数传值的时候需要使用地址。


sync.Mutex

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同一时间只有一个 goroutine 可以访问共享资源。

方法作用
Mutex.Lock()获取互斥锁
Mutex.Unlock()释放互斥锁

使用互斥锁对代码修改如下:

var (x   int64wg  sync.WaitGroupmtx sync.Mutex
)func function11() {add := func() {defer wg.Done()for i := 0; i < 5000; i++ {mtx.Lock()	// 修改数据前,加锁x = x + 1mtx.Unlock() // 修改完数据后,释放锁}}wg.Add(2)go add()go add()wg.Wait()fmt.Println(x) // 10000
}

sync.RWMutex

读写互斥锁,某些场景中读操作较为频繁,不涉及对数据的修改时,读写锁可能是更好的选择。

方法作用
RWMutex.Lock()获取写锁
RWMutex.Unlock()释放写锁
RWMutex.RLock()获取读锁
RWMutex.RUnlock()释放读锁

读写锁分为两种:读锁和写锁。当一个 goroutine 获取到读锁之后,其他的 goroutine 如果是获取读锁会继续获得锁,如果是获取写锁就会等待;而当一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待。


sync.Once

在高并发场景下,可以使用 sync.Once,保证操作只执行一次。当且仅当第一次访问某个变量时,进行初始化。变量初始化过程中,所有读都被阻塞,直到初始化完成。

sync.Once 其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的,并且初始化操作也不会被执行多次。

sync.Once 仅提供了一个方法 Do,参数 f 是对象初始化函数。

  • func (o *Once) Do(f func())

单例模式:

type Singleton struct{}var (instance *Singletononce     sync.Oncewg       sync.WaitGroup
)func GetInstance() *Singleton {once.Do(func() {instance = &Singleton{}fmt.Println("Get Instance")})return instance
}func function12() {for i := 0; i < 10; i++ {wg.Add(1)go func() {defer wg.Done()_ = GetInstance()}()} // Get Instancewg.Wait()
}

程序只会输出一次 Get Instance,说明 sync.Once 是线程安全的,支持并发,仅会执行一次初始化数据的函数。


sync.Map

Go 内置的 map 不是并发安全的,下述代码多个 goroutine 对 map 操作会出现竞态问题,报错不能正常运行。

var (mp = make(map[string]interface{})wg sync.WaitGroup
)func function13() {for i := 0; i < 10; i++ {wg.Add(1)go func() {defer wg.Done()key := strconv.Itoa(i)mp[key] = ifmt.Println(key, mp[key])}()}wg.Wait()
}

sync.Map 是并发安全版 map,不过操作数据不再是直接通过 [] 获取插入数据,而需要使用其提供的方法。

方法作用
Map.Store(key, value interface{})存储 key-value 数据
Map.Load(key interface{}) (value interface{}, ok bool)查询 key 对应的 value
Map.LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)查询 key 对应的 value,如果不存在则存储 key-value 数据
Map.LoadAndDelete(key interface{}) (value interface{}, loaded bool)查询并删除 key
Map.Delete(key interface{})删除 key
Map.Range(f func(key, value interface{}) bool)对 map 中的每个 key-value 依次调用 f

使用 sync.Map 修改上述代码,即可正确运行。

func function14() {m := sync.Map{}for i := 0; i < 10; i++ {wg.Add(1)go func() {defer wg.Done()key := strconv.Itoa(i)m.Store(key, i)v, ok := m.Load(key)if ok {fmt.Println(key, v)}}()}wg.Wait()
}

LoadOrStoreLoadAndDelete 示例代码:

// LoadOrStore、LoadAndDelete
func function15() {m := sync.Map{}//m.Store("cauchy", 19)v, ok := m.LoadOrStore("cauchy", 20)fmt.Println(v, ok) // 注释: 20 false;没注释: 19 truev, ok = m.Load("cauchy")fmt.Println(v, ok) // 注释: 20 true;没注释: 19 truev, ok = m.LoadAndDelete("cauchy")fmt.Println(v, ok) // 注释: 20 true;没注释: 19 truev, ok = m.Load("cauchy")fmt.Println(v, ok) // nil false
}

Range 示例代码:

Map.Range 可无序遍历 sync.Map 中的所有 key-value 键值对,如果返回 false 则终止迭代。

func function16() {m := sync.Map{}m.Store(3, 3)m.Store(2, 2)m.Store(1, 1)cnt := 0m.Range(func(key, value any) bool {cnt++fmt.Println(key, value)return true})fmt.Println(cnt) // 3
}

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

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

相关文章

【pytorch】函数记录

你好你好&#xff01; 以下内容仅为当前认识&#xff0c;可能有不足之处&#xff0c;欢迎讨论&#xff01; 文章目录 torch.sum()torch.argmax()torch.nn.Parametertorch.unbindtorch.optim.Adam()[^adam]torch.cattorch.unsqueeze()torch.normalize()[^l2]torch.eyetorch.mmto…

Elasticsearch使用function_score查询酒店和排序

需求 基于用户地理位置&#xff0c;对酒店做简单的排序&#xff0c;非个性化的推荐。酒店评分包含以下&#xff1a; 酒店类型&#xff08;依赖用户历史订单数据&#xff09;&#xff1a;希望匹配出更加符合用户使用的酒店类型酒店评分&#xff1a;评分高的酒店用户体验感好ge…

在Ubuntu22.04 LTS上搭建Kubernetes集群

文章目录 准备工作系统准备软件包准备主机和IP地址准备 安装步骤安装前准备关闭防火墙设置服务器时区关闭 swap 分区关闭SELinux配置hosts配置文件转发 IPv4 并让 iptables 看到桥接流量 安装容器运行时安装Kubernetes配置并安装apt包初始化集群 安装calico网络插件部署应用 本…

nvm下载node指定版本后npm不存在

一&#xff0c;项目背景 接手一个老的项目&#xff0c;需要使用旧的node版本&#xff0c;使用nvm下载12.11.0版本后发现npm命令不存在。 二&#xff0c;原因 查找资料发现是8.11以上版本的node版本对应的npm都没法自动安装&#xff0c;需要自己到npm官网( https://registry.…

详解Kotlin中run、with、let、also与apply的使用和区别

Kotlin作为一种现代、静态类型的编程语言&#xff0c;不仅提供了丰富的特性&#xff0c;还提供了极具表现力的函数&#xff1a;run, with, let, also, 和 apply。理解这些函数的不同之处对于编写高效、易于维护的代码至关重要。 函数对比表 函数对象引用返回值使用场景runthi…

DB-GPT:大模型 + 数据库,全流程自动化

DB-GPT&#xff1a;大模型 数据库&#xff0c;全流程自动化 提出背景DB-GPT 结构具体问题与解法背景分析对比其他工具DB-GPT系统设计 提出背景 论文&#xff1a;https://arxiv.org/pdf/2312.17449.pdf 代码&#xff1a;https://github.com/eosphoros-ai/DB-GPT 本文介绍了D…

二次供水物联网:HiWoo Cloud助力城市水务管理升级

随着城市化的快速推进&#xff0c;二次供水系统作为城市基础设施的重要组成部分&#xff0c;其稳定运行和高效管理显得至关重要。然而&#xff0c;传统的二次供水管理方式在应对复杂多变的城市供水需求时&#xff0c;显得力不从心。为了破解这一难题&#xff0c;HiWoo Cloud平台…

应用回归分析:弹性网络回归

弹性网络回归&#xff1a;原理、优势与应用 弹性网络回归&#xff08;Elastic Net Regression&#xff09;是一种广泛使用的线性回归方法&#xff0c;它结合了岭回归&#xff08;Ridge Regression&#xff09;和套索回归&#xff08;Lasso Regression&#xff09;的特点。通过…

等保2.0高风险项全解析:判定标准与应对方法

引言 所谓高风险项&#xff0c;就是等保测评时可以一票否决的整改项&#xff0c;如果不改&#xff0c;无论你多少分都会被定为不合格。全文共58页&#xff0c;写得比较细了&#xff0c;但是想到大家基本不会有耐心去仔细看的&#xff08;凭直觉&#xff09;。这几天挑里边相对…

Java学习--学生管理系统(残破版)

代码 Main.java import java.util.ArrayList; import java.util.Scanner;public class Main {public static void main(String[] args) {ArrayList<Student> list new ArrayList<>();loop:while (true) {System.out.println("-----欢迎来到阿宝院校学生管理系…

可视化图文报表

Apache Echarts介绍 Apache Echarts是一款基于Javascript的数据可视化图表库&#xff0c;提供直观&#xff0c;生动&#xff0c;可交互&#xff0c;可个性化定制的数据可视化图表。 官网&#xff1a;Apache ECharts 入门案例&#xff1a; <!DOCTYPE html> <html>…

C#,弗洛伊德-瑞文斯特(Floyd-Rivest)算法与源代码

Robert W. Floyd 1 Floyd-Rivest 算法 Floyd-Rivest 算法是一种选择算法&#xff0c;用于在不同元素的数组中找到第k个最小元素。它类似于快速选择算法&#xff0c;但在实际运行中有更好的运行时间。 和 QuickSelect 一样&#xff0c;该算法基于分区的思想工作。对数组进行分…

济南适宜地提取

题目: 网上下载中国的DEM、土地利用地图(1980、2000、2015年的)和一张最新济南市行政区划 图(要求:莱芜市并入济南后的区划图); 2.网上下载中国2015年年平均降水空间插值数据;3..网上下载中国2015年年平均气温空间插值数据; (注:以上数据可到资源环境科学与数据中心下载http…

【Docker】安装及相关的命令

目录 一 Docker简介 1.1 是什么 1.2 优缺点 1.3 应用场景 1.4 安装 二 命令 2.1 Docker基本命令 2.2 Docker镜像命令 2.3 Docker容器命令 一 Docker简介 1.1 是什么 Docker是一个开源的应用容器引擎&#xff0c;它基于Go语言实现&#xff0c;并利用操作系统本身已有的…

【QT+QGIS跨平台编译】之五十四:【QGIS_CORE跨平台编译】—【qgssqlstatementlexer.cpp生成】

文章目录 一、Flex二、生成来源三、构建过程一、Flex Flex (fast lexical analyser generator) 是 Lex 的另一个替代品。它经常和自由软件 Bison 语法分析器生成器 一起使用。Flex 最初由 Vern Paxson 于 1987 年用 C 语言写成。 “flex 是一个生成扫描器的工具,能够识别文本中…

状态机2——————

1.原理 2.代码 2.1 complex_fsm.v module complex_fsm(input wire sys_clk ,input wire sys_rst_n ,input wire pi_money_half ,input wire pi_money_one ,output reg po_cola ,output reg po_money );parameter IDLE 5b00001,HALF 5b00010,ONE 5b00100…

SQL注入漏洞解析-less-8(布尔盲注)

我们来看一下第八关 当我们进行尝试时&#xff0c;他只有You are in...........或者没有显示。 他只有对和错显示&#xff0c;那我们只能用对或者错误来猜他这个数据库 ?id1%27%20and%20ascii(substr(database(),1,1))>114-- ?id1%27%20and%20ascii(substr(database(),1,…

[WebUI Forge]ForgeUI的安装与使用 | 相比较于Auto1111 webui 6G显存速度提升60-75%

ForgeUI的github主页地址:https://github.com/lllyasviel/stable-diffusion-webui-forge Stable Diffusion WebUI Forge 是一个基于Stable Diffusion WebUI(基于Gradio)的平台,可简化开发、优化资源管理并加快推理速度。 “Forge”这个名字的灵感来自于“Minecraft Forge”…

【MySQL】MySQL复合查询--多表查询自连接子查询 - 副本 (2)

文章目录 1.基本查询回顾2.多表查询3.自连接4.子查询 4.1单行子查询4.2多行子查询4.3多列子查询4.4在from子句中使用子查询4.5合并查询 4.5.1 union4.5.2 union all 1.基本查询回顾 表的内容如下&#xff1a; mysql> select * from emp; ----------------------------…

vue项目导出excel ,文件过大导致请求超时的处理方法

一、因为文件过大&#xff0c;请求时间较长&#xff0c;就会产生请求超时的情况&#xff0c;处理方式是可以分为三个接口&#xff0c;接口1用来获取id值&#xff0c;接口2利用id值发起请求&#xff0c;询问是否准备好下载&#xff0c;如果没准备好&#xff0c;则没隔一秒再次发…