【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 := <-ch
	fmt.Println(v)
}

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

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

有缓冲通道

又称异步通道

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

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

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

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

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

func function06() {
	ch := make(chan int, 1)
	ch <- 1
	close(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 := 0
	for 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 two
	fmt.Println(time.Since(now)) // 2.0003655s
}

sync

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

sync.WaitGroup

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

var (
	x  int64
	wg 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   int64
	wg  sync.WaitGroup
	mtx sync.Mutex
)

func function11() {
	add := func() {
		defer wg.Done()

		for i := 0; i < 5000; i++ {
			mtx.Lock()	// 修改数据前,加锁
			x = x + 1
			mtx.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 *Singleton
	once     sync.Once
	wg       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 Instance
	wg.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] = i
			fmt.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 true
	v, ok = m.Load("cauchy")
	fmt.Println(v, ok) // 注释: 20 true;没注释: 19 true

	v, ok = m.LoadAndDelete("cauchy")
	fmt.Println(v, ok) // 注释: 20 true;没注释: 19 true
	v, 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 := 0
	m.Range(func(key, value any) bool {
		cnt++
		fmt.Println(key, value)
		return true
	})
	fmt.Println(cnt) // 3
}

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

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

相关文章

LoRa技术在智能气象监测中的应用与解决方案分享

LoRa技术在智能气象监测领域的应用具有广泛的前景&#xff0c;通过LoRa技术可以实现对气象数据的远程采集、传输和监测&#xff0c;为气象行业提供更加智能化和高效的解决方案。以下将探讨LoRa技术在智能气象监测中的应用与解决方案分享。 首先&#xff0c;LoRa技术可以用于连…

python|闲谈2048小游戏和数组的旋转及翻转和转置

目录 2048 生成数组 n阶方阵 方阵旋转 顺时针旋转 逆时针旋转 mxn矩阵 矩阵旋转 测试代码 测试结果 翻转和转置 2048 《2048》是一款比较流行​的数字游戏​&#xff0c;最早于2014年3月20日发行。原版2048由Gabriele Cirulli首先在GitHub上发布&#xff0c;后被移…

【C语言】数据存储篇,内存中的数据存储----C语言整型,浮点数的数据在内存中的存储以及大小端字节序【图文详解】

欢迎来CILMY23的博客喔&#xff0c;本篇为​【C语言】数据存储篇&#xff0c;内存中的数据存储----C语言整型&#xff0c;浮点数的数据在内存中的存储以及大小端字节序【图文详解】&#xff0c;感谢观看&#xff0c;支持的可以给个一键三连&#xff0c;点赞关注收藏。 前言 C语…

GIS之深度学习02:Anaconda2019版本安装(py38)

Anaconda是一个专注于数据科学和机器学习的开源发行版&#xff0c;内置了丰富的工具和库&#xff0c;包括Python解释器、NumPy、SciPy、Pandas、Scikit-learn、TensorFlow等&#xff0c;使用户能够轻松进行科学计算和数据分析。其强大的包管理器conda简化了软件包的安装和环境管…

Linux/Spectra

Enumeration nmap 第一次扫描发现系统对外开放了22&#xff0c;80和3306端口&#xff0c;端口详细信息如下 22端口运行着ssh&#xff0c;80端口还是http&#xff0c;不过不同的是打开了mysql的3306端口 TCP/80 进入首页&#xff0c;点击链接时&#xff0c;提示域名不能解析&…

4核8G服务器并发数多少?性能如何?

腾讯云4核8G服务器支持多少人在线访问&#xff1f;支持25人同时访问。实际上程序效率不同支持人数在线人数不同&#xff0c;公网带宽也是影响4核8G服务器并发数的一大因素&#xff0c;假设公网带宽太小&#xff0c;流量直接卡在入口&#xff0c;4核8G配置的CPU内存也会造成计算…

vue3 增加全局水印(显示登录信息)

一、纯文字水印 在main.ts页面里面 加入以下代码&#xff1a; // 导入 Vue 的 createApp 函数 import { createApp } from vue;// 导入全局样式文件 import ./style.css;// 导入根组件 App.vue import App from ./App.vue;// 导入路由配置 import router from ./router;// 使…

VUE从0到1创建项目及基本路由、页面配置

一、创建项目:(前提已经安装好vue和npm) 目录:E:\personal\project_pro\ windows下,win+R 输入cmd进入命令行: cd E:\personal\project_pro E:# 创建名为test的项目 vue create test# 用上下键选择vue2或vue3,回车确认创建本次选择VUE3 创建好项目后,使用…

【CSS-语法】

CSS-语法 ■ CSS简介■ CSS 实例■ CSS id 和 class选择器■ CSS 样式表■ 外部样式表(External style sheet)■ 内部样式表(Internal style sheet)■ 内联样式(Inline style)■ 多重样式 ■ CSS 文本■ CSS 文本颜色■ CSS 文本的对齐方式■ CSS 文本修饰■ CSS 文本转换■ CS…

2024智慧城市革命:人工智能、场景与运营的融合之力

在数字革命的浪潮中&#xff0c;2024年的智慧城市将成为人类社会进步的新地标。 三大关键元素——人工智能、场景应用和精准运营——正在重新塑造城市面貌&#xff0c;构建未来的智慧城市生活图景。 一、人工智能&#xff1a;赋能智慧城市 随着人工智能技术的快速发展&#x…

第十二篇【传奇开心果系列】Python文本和语音相互转换库技术点案例示例:深度解读SpeechRecognition语音转文本

传奇开心果系列 系列博文目录Python的文本和语音相互转换库技术点案例示例系列 博文目录前言一、SpeechRecognition语音转文本一般的操作步骤和示例代码二、SpeechRecognition 语音转文本的优势和特点三、易用性深度解读和示例代码四、多引擎支持深度解读和示例代码五、灵活性示…

VL817-Q7 USB3.0 HUB芯片 适用于扩展坞 工控机 显示器

VL817-Q7 USB3.1 GEN1 HUB芯片 VL817-Q7 USB3.1 GEN1 HUB芯片 VIA Lab的VL817是一款现代USB 3.1 Gen 1集线器控制器&#xff0c;具有优化的成本结构和完全符合USB标准3.1 Gen 1规范&#xff0c;包括ecn和2017年1月的合规性测试更新。VL817提供双端口和双端口4端口配置&…

Alist访问主页显示空白解决方法

文章目录 问题记录问题探索和解决网络方案问题探究脚本内容查看 最终解决教程 问题记录 访问Alist主页显示空白&#xff0c;按F12打开开发人员工具 ,选择控制台&#xff0c;报错如下 index.75e31196.js:20 Uncaught TypeError: Cannot assign to read only property __symbo…

我来告诉你,为什么你的第一份工作要去大厂

选择第一份工作&#xff0c;就像是为你的职业生涯设置航向&#xff0c;起点往往决定了你能飞得多高。 为什么说走进大厂是一个明智的决策呢&#xff1f; 简单来说&#xff0c;大厂不仅是一个工作的地方&#xff0c;它是一个成长的加速器&#xff0c;一个能让你的能力和视野快速…

2023年清洁纸品行业分析报告:线上市场销额突破124亿,湿厕纸为重点增长类目

如今&#xff0c;清洁纸品早已经成为人们日常生活的必需品&#xff0c;其市场规模也比较庞大。从销售数据来看&#xff0c;尽管2023年清洁纸品市场整体的销售成绩呈现下滑&#xff0c;但其市场体量仍非常大。 鲸参谋数据显示&#xff0c;2023年京东平台上清洁纸品市场的销量将…

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

文章目录 一、Bison二、生成来源三、构建过程一、Bison GNU Bison 是一个通用的解析器生成器,它可以将注释的无上下文语法转换为使用 LALR (1) 解析表的确定性 LR 或广义 LR (GLR) 解析器。Bison 还可以生成 IELR (1) 或规范 LR (1) 解析表。一旦您熟练使用 Bison,您可以使用…

分享three.js和cannon.js构建Web 3D场景

使用 three.js&#xff0c;您不再需要花哨的游戏PC或控制台来显示逼真的3D图形。 您甚至不需要下载特殊的应用程序。现在每个人都可以使用智能手机和网络浏览器体验令人惊叹的3D应用程序。 这个惊人的库和充满活力的社区是您在浏览器、笔记本电脑、平板电脑或智能手机上创建游…

Flink SQL 中的流式概念:状态算子

博主历时三年精心创作的《大数据平台架构与原型实现&#xff1a;数据中台建设实战》一书现已由知名IT图书品牌电子工业出版社博文视点出版发行&#xff0c;点击《重磅推荐&#xff1a;建大数据平台太难了&#xff01;给我发个工程原型吧&#xff01;》了解图书详情&#xff0c;…

认识AJAX

一、什么是Ajax? 有跳转就是同步&#xff0c;无跳转就是异步 Asynchronous Javascript And XML&#xff08;异步JavaScript和XML&#xff09; Ajax 异步 JavaScript 和XML。Ajax是一种用于创建快速动态网页的技术通过在后台与服务器进行少量数据交换&#xff0c;Ajax可以使网…

Python手册(Machine Learning)--LightGBM

Overview LightGBM&#xff08;Light Gradient Boosting Machine&#xff09;是一种高效的 Gradient Boosting 算法&#xff0c; 主要用于解决GBDT在海量数据中遇到的问题&#xff0c;以便更好更快的用于工业实践中。 数据结构说明lightgbm.DatasetLightGBM数据集lightgbm.Bo…