Go协程与通道的综合应用问题

1.简单了解什么是协程和通道

什么是协程

协程,是一种用户级的轻量级的线程,拥有独立的栈空间并共享程序的堆空间。

它是在单线程的基础上通过算法来实现的微线程,相比于多线程编程具有以下优点:

  • 协程的上下文切换由用户决定,无需系统内核的上下文切换,减少开销
  • 协程默认会做好全方位保护,以防止中断。无需原子操作锁
  • 单线程也可以实现高并发,甚至达到单核CPU就支持上万协程

什么是通道

通道,是一种用于协程之间进行通信的数据结构。类似于队列,一端为发送者,一端为接收者。使用通道可以很好地保证数据的同步性和顺序性。

通道分为有缓冲通道和无缓冲通道,其声明方式如下:

  • 有缓冲通道
intChan := make(chan int,<缓冲容量>)
  • 无缓冲通道
intChan := make(chan int)

有缓冲通道和无缓冲通道的区别:

  • 阻塞:无缓冲通道发送者会阻塞直到数据被接收;有缓冲通道发送者会阻塞直到缓冲区满,接收者会阻塞直到缓冲区不为空。
  • 数据同步和顺序:无缓冲通道保证数据的同步和顺序;有缓冲管道不保证数据的同步和顺序。
  • 应用场景:无缓冲通道要求严格的同步和顺序性;有缓冲通道可以异步通信并提高吞吐量。

在无缓冲通道的实现中需要注意的是,通道的两端必须存在发送者和接收者,否则会导致死锁。

2.协程-通道并发编程案例

(1)交替打印字母和数字

题意:使用协程-通道交替打印数字1-10和字母A-J。

代码:

package mainimport ("fmt""sync"
)/*
无缓冲chanel:需要在写入chanel的时候要保证有另外一个协程在读取chanel。否则会导致写端阻塞,发生死锁
解决办法:
避免死锁的发生:
当i循环到10时,printAlp协程已然结束,所以此时不必再写入alp通道
*/func printNum(wg *sync.WaitGroup, numCh chan struct{}, alpCh chan struct{}) {defer wg.Done()for i := 1; i <= 10; i++ {<-alpCh // 等待字母goroutine发信号fmt.Print(i, " ")//避免死锁发生if i < 10 {numCh <- struct{}{} // 发信号给字母goroutine}if i == 10 {close(numCh)}}}func printAlp(wg *sync.WaitGroup, numCh chan struct{}, alpCh chan struct{}) {defer wg.Done()for i := 'A'; i <= 'J'; i++ {<-numCh // 等待数字goroutine发信号fmt.Printf("%c", i)alpCh <- struct{}{} // 发信号给数字goroutine}close(alpCh)
}func main() {numCh := make(chan struct{}) // 用于数字goroutine的信号通道alpCh := make(chan struct{}) // 用于字母goroutine的信号通道var wg sync.WaitGroupwg.Add(2)go printAlp(&wg, numCh, alpCh)go printNum(&wg, numCh, alpCh)// 启动时先给数字goroutine发送一个信号numCh <- struct{}{}wg.Wait()}

题目分析:

题目要求我们交替打印字母和数字,则需要保证两个协程的严格顺序性,符合无缓冲通道的应用场景。设置两个通道,分别存储数字和字母,两个打印数字和字母的协程分别担任两个通道的发送者和接收者的两重身份。循环打印一次,发一次信号,提醒另一个协程进行打印。

需要注意的是当最后一个字符'10'打印结束后,此时打印字母的协程已经结束,numCh通道已经没有接收者,此时已经不符合无缓冲通道的实现条件-必须存在发送者和接收者,再发送信号,会引起阻塞死锁。所以再第10次时不必再发送信号。

(2)设计一个任务调度器

题目:设计一个任务调度器,利用多协程+通道的编程模式,实现并发处理多任务的业务场景,且要求调度顺序按照任务添加顺序。

代码:

type scheduler struct {taskChan chan func()wt       sync.WaitGroup
}func (td *scheduler) AddTask(task func()) {td.taskChan <- task
}func (td *scheduler) Executer() {defer td.wt.Done()for {task, ok := <-td.taskChantask()if ok && len(td.taskChan) == 0 {break}}
}func (td *scheduler) Start() {td.wt.Add(4)//假设四个消费者for i := 0; i < 4; i++ {go td.Executer()}td.wt.Wait()
}func main() {sd := scheduler{taskChan: make(chan func(), 5),}go func() {sd.AddTask(func() {fmt.Println("任务1")})sd.AddTask(func() {fmt.Println("任务2")})sd.AddTask(func() {fmt.Println("任务3")})sd.AddTask(func() {fmt.Println("任务4")})sd.AddTask(func() {fmt.Println("任务5")})sd.AddTask(func() {fmt.Println("任务6")})close(sd.taskChan)}()sd.Start()}

问题分析:

由于添加的任务为多任务,不止一个,并且需要异步处理执行这些任务。符合有缓冲区的通道需要提高吞吐量和异步处理。

那么,我们需要将任务放进通道,多个接收者,按照顺序从通道中拿任务,并进行执行即可。

需要注意的问题是,如果在添加的任务数量大于通道的缓冲区,会在添加任务形成阻塞。为了不影响消费者的正常启动,需要将其单独开一个协程来添加任务。

这样当消费者进行消费时,形成阻塞的生产者会被唤醒,从而继续进行任务添加。

3.总结

经过对协程+通道的编程模式的学习,除了刚刚在题目中提到的,我们还应该注意以下问题:

1.为什么通道用完之后要关闭,不关闭有什么风险?

  • 为了避免死锁。关闭通道,也是告诉接收者,在发送者那里已经没有数据可以发送了,不需要再继续等待数据了。接收者收到通道关闭的信息后,停止接收数据;若不关闭通道,则会让接收者一直处于阻塞状态,有发生死锁的风险。
  • 释放资源和避免资源泄露。关闭通道后,系统会释放相应的资源,及时关闭通道则可以避免资源浪费和泄露。 

2. 怎么优雅地关闭通道?

首先,关闭通道的最基本原则是不要关闭已经关闭的通道。其次还有一个使用Go通道的原则:不要在数据接收方或者在有多个发送者的情况下关闭通道。换句话说我们只应该让一个通道唯一的发送者关闭此通道。

一种粗鲁的方式是通过异常恢复的方式来关闭通道,但很明显违反以上的原则且有可能发生数据竞争;另一种方式是sync.Once或sync.Mutex来关闭通道,当并不保证发生在一个通道上的并发关闭操作和发送操纵不会产生数据竞争。这两种方式都有一定的问题,就不过多介绍,下面介绍一种如何优雅地关闭通道的方法。

情形一:M个接收者和一个发送者

最容易处理的一种情形。当发送者需要结束发送时,让它关闭通道即可。上文的两个编程案例就是这种情形。

情形二:一个接收者和N个发送者

根据Go通道的基本原则,我们只能在通道的唯一发送者关闭通道。所以,在这种情况下,我们无法直接在某处关闭通道。但我们可以让接收者关闭一个额外的信号通道来告诉发送者不要再发送数据了

package mainimport ("log""sync"
)func main() {cosnt N := 5cosnt Max := 60000count := 0dataCh := make(chan int)stopCh := make(chan bool)var wt sync.WaitGroupwt.Add(1)//发送者for i := 0; i < N; i++ {go func() {for {select {case <-stopCh:returndefault:count += 1dataCh <- count}}}()}//接收者go func() {defer wt.Done()for value := range dataCh {if value == Max {// 此唯一的接收者同时也是stopCh通道的// 唯一发送者。尽管它不能安全地关闭dataCh数// 据通道,但它可以安全地关闭stopCh通道。close(stopCh)return}log.Println(value)}}()wt.Wait()
}

在这种方法中,我们额外增加了一个信号通道stopCh,在接收方通过它来告诉发送者不必再接收数据。并且,此方法并没有对dataCh进行关闭,当一个通道不再被任何协程使用时,它将会逐渐被垃圾回收掉,无论它是否已经被关闭。

此方法的优雅性就在于通过关闭一个通道来停止另一个通道的使用,从而间接关闭另一个通道。

情形三:M个接收者N个发送者

我们不能让接收者和发送者中的任何一个关闭用来传输数据的通道,我们也不能让多个接收者之一关闭一个额外的信号通道。这两种做法都违反了通道关闭原则。

不过,我们可以引入一个中间调解者角色并让其关闭额外的信号通道来通知所有接收者和发送者结束工作

代码示例:

package mainimport ("log""math/rand""strconv""sync"
)func main() {const Max = 100000const NumReceivers = 10const NumSenders = 1000var wt sync.WaitGroupwt.Add(NumReceivers)dataCh := make(chan int)stopCh := make(chan struct{})// stopCh是一个额外的信号通道。它的发送// 者为中间调解者。它的接收者为dataCh// 数据通道的所有的发送者和接收者。toStop := make(chan string, 1)// toStop是一个用来通知中间调解者让其// 关闭信号通道stopCh的第二个信号通道。// 此第二个信号通道的发送者为dataCh数据// 通道的所有的发送者和接收者,它的接收者// 为中间调解者。它必须为一个缓冲通道。var stoppedBy string// 中间调解者go func() {stoppedBy = <-toStopclose(stopCh)}()// 发送者for i := 0; i < NumSenders; i++ {go func(id string) {for {value := rand.Intn(Max)if value == 0 {// 为了防止阻塞,这里使用了一个尝试// 发送操作来向中间调解者发送信号。select {case toStop <- "发送者:" + id:default:}return}select {case <-stopCh:returncase dataCh <- value:}}}(strconv.Itoa(i))}// 接收者for i := 0; i < NumReceivers; i++ {go func(id string) {defer wt.Done()for {select {case <-stopCh:returncase value := <-dataCh:if value == Max {// 发送操作来向中间调解者发送信号。select {case toStop <- "接收者:" + id:default:}return}log.Println(value)}}}(strconv.Itoa(i))}wt.Wait()log.Println("被" + stoppedBy + "终止了")}

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

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

相关文章

基于Go1.19的站点模板爬虫详细介绍

构建一个基于Go1.19的站点模板爬虫是一项有趣且具有挑战性的任务。这个爬虫将能够从网站上提取数据,并按照指定的模板进行格式化。以下是详细的介绍和实现步骤。 1. 准备工作 工具和库: Go 1.19colly:一个强大的Go爬虫库goquery:一个类似于 jQuery 的Go库,用于解析 HTML…

1071 - Specified key was too long; max key length is 3072 bytes Mysql报错解决方法

错误信息 “Specified key was too long; max key length is 3072 bytes” 是在MySQL数据库中创建索引时可能出现的问题&#xff0c;通常出现在尝试创建一个过长的唯一键&#xff08;UNIQUE KEY&#xff09;或主键&#xff08;PRIMARY KEY&#xff09;时。MySQL对于InnoDB存储引…

Codeforces Round 957 (Div.3)

传送门 A. Only Pluses 时间限制&#xff1a;1秒 空间限制&#xff1a;256MB 输入&#xff1a;标准输入 输出&#xff1a;标准输出 问题描述 Kmes 写下了三个整数 a、b 和 c&#xff0c;以记住他要给 Noobish_Monk 的香蕉数量是 a b c。 Noobish_M…

vue3<script setup>自定义指令

main.ts // 自定义指令 app.directive(color,(el,binding) > {el.style.color binding.value })这段代码定义了一个名为color的自定义指令&#xff0c;并将其注册到Vue应用实例app上。自定义指令接收两个参数&#xff1a;el和binding。el是绑定指令的元素&#xff0c;而bi…

Ubuntu22.04安装NIVIDIA显卡驱动总结

1.首先在安装驱动时需要判断系统有无GPU以及GPU的型号 可以参考这篇文章&#xff1a; https://blog.51cto.com/u_13171517/8814753#:~:textubuntu%20%E7%B3%BB%E7%BB%9F%20%E6%80%8E%E4%B9%88%E5%88%A4%E6%96%AD%E7%B3%BB%E7%BB%9F%E6%9C%89%E6%B2%A1%E6%9C%89GPU%201%20%E6%…

【C++】函数重载详解

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &#x1f4e2;本文由 JohnKi 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f4e2;未来很长&#…

【LLM大模型】Langchain 介绍与入门

官方介绍 LangChain 是一个利用LLM开发应用程序的框架。它让应用程序具备&#xff1a; 上下文感知能力&#xff1a;将LLM连接到上下文源&#xff08;提示说明、少量示例、用以形成其响应的内容等&#xff09;推理&#xff1a;依靠LLM进行推理&#xff08;例如根据提供的上下文…

全网最详细单细胞保姆级分析教程

各位读者,好久不见,我又归来了,之后的一段时候我将以Rstudio分析单细胞的RNA-seq流程为主,希望各位读者朋友多多支持! 1. pbmc单样本分析 1.包的加载 library(multtest) library(dplyr) library(Seurat) library(patchwork) library(R.utils)2. 清除环境变量 rm(list ls))…

深度解析蚂蚁 SEO 蜘蛛池:提升网站流量的有效利器

在当今数字化时代&#xff0c;网站流量对于企业和个人的在线业务成功至关重要。为了在竞争激烈的网络环境中脱颖而出&#xff0c;众多站长和 SEO 从业者不断探索各种优化策略&#xff0c;其中蚂蚁 SEO 的蜘蛛池成为备受关注的工具之一。 蚂蚁 SEO 蜘蛛池是一种创新的技术手段&a…

HarmonyOS鸿蒙开发入门 , ArkTS语言的了解

鸿蒙&#xff08;即HarmonyOS&#xff0c;开发代号Ark&#xff0c;正式名称为华为终端鸿蒙智能设备操作系统软件&#xff09;是由华为公司2012年以来开发的分布式操作系统&#xff0c;并于2019年8月正式发布。该系统利用“分布式”技术&#xff0c;将手机、电脑、平板、电视、汽…

画封装步骤

parameter参数 1.打开pad designer 2.设计单位mils改为millimeter&#xff0c;保留decimal layers 3.勾选☑️single layer mode

游戏厅ps5体验馆计时收费软件 佳易王电玩馆计时器定时语音提醒系统操作教程

前言&#xff1a; 游戏厅ps5体验馆计时收费软件 佳易王电玩馆计时器定时语音提醒系统操作教程 以下软件操作教程以&#xff0c;佳易王游戏厅电玩店计时计费管理系统软件为例说明 软件文件下载可以点击最下方官网卡片——软件下载——试用版软件下载 一、软件操作教程 1、计…

【Python】人生重开模拟器(实现代码)

一、游戏背景介绍 这是一款文字类小游戏。玩家输入角色的初始属性之后&#xff0c;就可以开启不同的人生经历。 完整的程序代码较多&#xff0c;此这里只实现其中的一部分逻辑&#xff08;主要目的&#xff1a;巩固前面学习的 Python 语法基础&#xff09;。 二、设置初始属性…

springboot企业人力资源管理系统-计算机毕业设计源码29005

目录 摘要 1 绪论 1.1 选题背景与意义 1.2国内外研究现状 1.3论文结构与章节安排 2系统分析 2.1 可行性分析 2.2 系统流程分析 2.2.1系统开发流程 2.2.2 用户登录流程 2.2.3 系统操作流程 2.2.4 添加信息流程 2.2.5 修改信息流程 2.2.6 删除信息流程 2.3 系统功能…

Redis 主从复制,哨兵与集群

目录 一.redis主从复制 1.redis 主从复制架构 2.主从复制特点 3.主从复制的基本原理 4.命令行配置 5.实现主从复制 6.删除主从复制 7.主从复制故障恢复 8.主从复制完整过程 9.主从同步优化配置 二.哨兵模式&#xff08;Sentinel&#xff09; 1.主要组件和概念 2.哨…

基于复旦微V7 690T FPGA +ARM/海光X86+AI的全国产化数据采集人工智能平台

国产化FPGA&#xff1a;JFM7VX690T80主机接口&#xff1a;PCIe Gen3 x88Gbps/lane光纤通道&#xff1a;前面板4路SFP光纤&#xff0c;后面板1路QSFP光纤2组独立的DDR3 SDRAM 缓存&#xff0c;工作时钟频率800MHz2个FMC接口扩展&#xff1a;每个支持16路GTH&#xff0c;线速率10…

提示词工程(Prompt Engineering)是什么?

一、定义 Prompt Engineering 提示词工程&#xff08;Prompt Engineering&#xff09;是一项通过优化提示词&#xff08;Prompt&#xff09;和生成策略&#xff0c;从而获得更好的模型返回结果的工程技术。 二、System message 系统指令 System message可以被广泛应用在&am…

子载波间隔如何确定

OFDM子载波间隔公式 在OFDM系统中,子载波间隔Δf的基本公式为: Δf 1 / T 其中: Δf 是子载波间隔(Hz)T 是OFDM符号周期(秒) 公式解释 这个公式源于保持子载波正交性的需求。 当子载波间隔等于OFDM符号速率的倒数时,可以实现最小的频谱重叠,同时保持正交性。 这个间隔确…

ORB-slam3 安装教程

1. 官网下载源码&#xff1a;GitHub - UZ-SLAMLab/ORB_SLAM3: ORB-SLAM3: An Accurate Open-Source Library for Visual, Visual-Inertial and Multi-Map SLAM 2. 根据官网下载依赖&#xff1a; &#xff08;1&#xff09;eigen3:Eigen 解压后进入源码目录进行编译&#xff1a…

ensp实验:防火墙安全策略用户认证综合策略

实验要求&#xff1a; 示例图&#xff1a; 设备配置&#xff1a; LSW5 vlan配置&#xff1a; 防火墙网络配置&#xff1a; 安全区域配置&#xff1a; 地址组配置&#xff1a; 时钟配置: 一&#xff1a; 办公区策略&#xff1a; 生产区策略&#xff1a; 二&#xff1a; 游客区…