【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://xiahunao.cn/news/2814601.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

雾锁王国服务器配置怎么选择?阿里云和腾讯云

雾锁王国/Enshrouded服务器CPU内存配置如何选择&#xff1f;阿里云服务器网aliyunfuwuqi.com建议选择8核32G配置&#xff0c;支持4人玩家畅玩&#xff0c;自带10M公网带宽&#xff0c;1个月90元&#xff0c;3个月271元&#xff0c;幻兽帕鲁服务器申请页面 https://t.aliyun.com…

Firefox Focus,一个 “专注“ 的浏览器

近期才开始使用 Firefox Focus&#xff0c;虽然使用频率其实并不高&#xff0c;基本上只有想到了才去用&#xff0c;但每次使用的体验都很不错。 Firefox Focus 这款浏览器大约在 2015 年首次发布&#xff0c;不同于一般版本的 Firefox&#xff0c;它主打“自动删除浏览记录”…

Python请求示例获取淘宝商品详情数据API接口,item_get-获得淘宝商品详情(按关键词搜索商品列表)

请求示例&#xff0c;API接口接入Anzexi58 item_get-获得淘宝商品详情 公共参数 名称类型必须描述keyString是调用key&#xff08;必须以GET方式拼接在URL中&#xff09;secretString是调用密钥WeChat18305163218api_nameString是API接口名称&#xff08;包括在请求地址中&am…

阅读笔记——《GANFuzz: A GAN-based industrial network protocol fuzzing framework》

【参考文献】Hu Z, Shi J, Huang Y H, et al. GANFuzz: a GAN-based industrial network protocol fuzzing framework[C]//Proceedings of the 15th ACM International Conference on Computing Frontiers. 2018: 138-145.【注】本文仅为作者个人学习笔记&#xff0c;如有冒犯&…

66-ES6:var,let,const,函数的声明方式,函数参数,剩余函数,延展操作符,严格模式

1.JavaScript语言的执行流程 编译阶段&#xff1a;构建执行函数&#xff1b;执行阶段&#xff1a;代码依次执行 2.代码块&#xff1a;{ } 3.变量声明方式var 有声明提升&#xff0c;允许重复声明&#xff0c;声明函数级作用域 访问&#xff1a;声明后访问都是正常的&…

ElasticSearch之找到乔丹的空中大灌篮电影

写在前面 本文看一个搜索的实际例子&#xff0c;找到篮球之神乔丹的电影Space Jam&#xff0c;即空中大灌篮。 正式开始之前先来看下要查询的目标文档&#xff0c;以及查询的text&#xff1a; 要查询的目标文档 {..."title": "Space Jam",..."ove…

密码学系列(四)——对称密码2

一、RC4 RC4&#xff08;Rivest Cipher 4&#xff09;是一种对称流密码算法&#xff0c;由Ron Rivest于1987年设计。它以其简单性和高速性而闻名&#xff0c;并广泛应用于网络通信和安全协议中。下面是对RC4的详细介绍&#xff1a; 密钥长度&#xff1a; RC4的密钥长度可变&am…

数据结构:栈和队列与栈实现队列(C语言版)

目录 前言 1.栈 1.1 栈的概念及结构 1.2 栈的底层数据结构选择 1.2 数据结构设计代码&#xff08;栈的实现&#xff09; 1.3 接口函数实现代码 &#xff08;1&#xff09;初始化栈 &#xff08;2&#xff09;销毁栈 &#xff08;3&#xff09;压栈 &#xff08;4&…

Unity(第九部)物体类

拿到物体的某些数据 using System.Collections; using System.Collections.Generic; using UnityEngine;public class game : MonoBehaviour {// Start is called before the first frame updatevoid Start(){//拿到当前脚本所挂载的游戏物体//GameObject go this.gameObject;…

踩坑wow.js 和animate.css一起使用没有效果

踩坑wow.js 和animate.css一起使用没有效果 问题及解决方法一、电脑系统配置问题二、版本问题 问题及解决方法 一、电脑系统配置问题 在系统属性里面把窗口内的动画和元素勾选 二、版本问题 使用wow加animate4.4.1也就是最新本&#xff0c;打开网页没有任何动画效果 但是把…

CSS——PostCSS简介

文章目录 PostCSS是什么postCSS的优点补充&#xff1a;polyfill补充&#xff1a;Stylelint PostCSS架构概述工作流程PostCSS解析方法PostCSS解析流程 PostCSS插件插件的使用控制类插件包类插件未来的CSS语法相关插件后备措施相关插件语言扩展相关插件颜色相关组件图片和字体相关…

45、上海大学:轻量级多特征神经网络M-FANet,用于MI-BCI解码

本文由上海大学机电工程与自动化学院于2024.1.9日发表于《IEEE Transactions on Neural Systems and Rehabilitation Engineering》(SCI中科院分区二区&#xff0c;IF&#xff1a;4.9) 论文链接&#xff1a;M-FANet: Multi-Feature Attention Convolutional Neural Network fo…

数据结构--二叉排序树(Binary Search Tree,简称BST)

这里写自定义目录标题 二叉排序树二叉排序树与排序数组没有排序数组&#xff0c;链式存储链表的对比二叉排序树概念对于搜索操作&#xff0c;对于插入操作&#xff0c;对于删除操作&#xff0c; 分析删除节点代码运行结果 二叉排序树 二叉排序树与排序数组没有排序数组&#x…

【React源码 - 调度任务循环EventLoop】

我们知道在React中有4个核心包、2个关键循环。而React正是在这4个核心包中运行&#xff0c;从输入到输出渲染到web端&#xff0c;主要流程可简单分为一下4步&#xff1a;如下图&#xff0c;本文主要是介绍两大循环中的任务调度循环。 4个核心包&#xff1a; react&#xff1a;…

SpringMVC了解

1.springMVC概述 Spring MVC&#xff08;Model-View-Controller&#xff09;是基于 Java 的 Web 应用程序框架&#xff0c;用于开发 Web 应用程序。它通过将应用程序分为模型&#xff08;Model&#xff09;、视图&#xff08;View&#xff09;和控制器&#xff08;Controller&a…

SpringMVC 学习(八)之文件上传与下载

目录 1 文件上传 2 文件下载 1 文件上传 SpringMVC 对文件的上传做了很好的封装&#xff0c;提供了两种解析器。 CommonsMultipartResolver&#xff1a;兼容性较好&#xff0c;可以兼容 Servlet3.0 之前的版本&#xff0c;但是它依赖了 commons-fileupload …

kubectl 命令行管理K8S(上)

目录 陈述式资源管理方式 介绍 命令 项目的生命周期 创建 kubectl create命令 发布 kubectl expose命令 更新 kubectl set 回滚 kubectl rollout 删除 kubectl delete 应用发布策略 金丝雀发布 陈述式资源管理方式 介绍 1.kubernetes 集群管理集群资源…

python 基础知识点(蓝桥杯python科目个人复习计划53)

今日复习内容&#xff1a;做题 例题1&#xff1a;最大的卡牌价值 问题描述&#xff1a; 给定n副卡牌&#xff0c;每张卡牌具有正反面&#xff0c;正面朝上数字为ai&#xff0c;背面朝上数字为bi。一副卡牌的价值为正面朝上数字之和&#xff0c;一开始所有卡牌都是正面朝上的…

【已解决】用ArcGIS处理过的数据在QGIS中打开发生偏移怎么办?| 数据在ArcGIS中打开位置正常,在QGIS中偏移

1. 问题描述 栅格或者矢量数据用ArcGIS打开时位置正确&#xff08;可以和其他数据对应上&#xff09;。但是用QGIS打开后发现位置不对 2. 问题的原因 因为该数据用了ArcGIS自定义的坐标系&#xff0c;QGIS不支持&#xff0c;识别有误。因此在数据QGIS中的坐标系参数有误&a…

HTTP 的 multipart 类型

上一篇文章讲到 http 的 MIME 类型 http MIME 类型 里有一个 multipart 多部分对象集合类型&#xff0c;这个类型 http 指南里有讲到&#xff1a;MIME 中的 multipart&#xff08;多部分&#xff09;电子邮件报文中包含多个报文&#xff0c;它们合在一起作为单一的复杂报文发送…