逐步学习Go-协程goroutine

file

参考:逐步学习Go-协程goroutine – FOF编程网

什么是线程?

简单来说线程就是现代操作系统使用CPU的基本单元。线程基本包括了线程ID,程序计数器,寄存器和线程栈。线程共享进程的代码区,数据区和操作系统的资源。

线程为什么很“重”

因为线程会有很多上下文,操作系统需要调度线程执行不能让一个线程执行完才执行另一个线程(那其他线程就“饿死”了)。
线程调度就涉及线程切换:停止当前正在运行的线程,保存线程的状态(上下文),选择另一个线程并加载这个上下文并执行这个线程。线程切换比较耗时因为内核或者操作系统级别的线程有很多上下文,主要涉及的切换有:

  1. 程序计数器
  2. 寄存器
  3. CPU缓存
  4. CPU调度
  5. 线程状态管理

所以线程切换比较耗时。

什么是协程?

协程并不是一个新概念了,协程已经被很多语言使用了,比如:Java 19的VirtualThread,Python的asyncio, JavaScript ES6中的async/await, C#, Kotlin,…

协程就是轻量级线程,协程和操作系统的重量级线程的关系是M:N,一般M\<N。减少调度和切换开销。
协程还有什么优势?

  1. 内存占用小,据Go说,Go创建一个协程只需要2KB内存
  2. 切换成本低,线程切换只涉及用户程序的调度,不涉及线程哪些切换的内容,所以很快
  3. 创建销毁快,用户程序创建和销毁,所以很快

协程和线程的映射关系

我们可以把线程是协程的CPU,协程需要执行需要调度到某个线程上执行。
协程最终还是使用线程来执行,所以协程需要对应一个线程来执行自己的代码,那么这个映射关系是什么?

  1. 一对一
  2. 一对多
  3. 多对一
  4. 多对多

一对一

如何来理解一对一关系?我觉得这是在某一时刻,一个协程都由一个线程来管理和执行。

一对多

如果理解一对多关系?我觉得这是在一个时间段内,一个协程可能会被调度到多个线程上执行,但是在某一个时间点一个协程不会被调度到两个或者更多线程执行。

多对一

如何理解多对一关系?我觉得是多个协程在一个时间段内会被调度到同一个线程执行。

多对多

协程运行时是M:N模型,就是M个协程映射到N个线程上。

Go中的协程

进入正题,Go中提供了协程模型和API,没有可以直接操作的线程模型和API。

Go的协程特性

Go的协程遵守我们上面提到的协程特性:

  1. 轻量级
  2. 并发执行
  3. 异步执行
  4. 复用:这个复用指的是复用操作系统线程
  5. 协程之间通过Channel通信和同步
  6. 非抢占式调度:Go的协程调度器使用的是非抢占式调度模型,这就表示协程在运行期间是不可中断的,只有协程自己让出CPU,比如:协程休眠,I/O之类的操作协程才会让出CPU
  7. 高效上下文切换
  8. 优雅关闭
  9. 不阻塞主线程,主线程退出,协程也会退出

环境

我们使用go testing和testify来编写测试用例进行协程特性演示。
testify直接使用go get安装就可以了。

go get github.com/stretchr/testify

COPY

这是import的模块:

import ("fmt""runtime""testing""github.com/stretchr/testify/assert"
)

COPY

创建协程

go中创建协程不需要写接口,不需要写struct,只需要一个go关键字+执行函数就可以了。

  1. go+标准函数
  2. go+闭包/匿名函数
  3. go+方法(struct)
  4. interface{}+反射
  5. 如有其他方式,请留言告知

go+标准函数创建协程

我们先来创建一个Go函数,参数传入一个channel方便我们对channel进行同步控制:

// 标准Go函数
func standardFunc(ch chan bool) {println("Hello, Standard Function Go Routine")ch <- true
}

COPY

我们来创建一个Go协程来执行这个标准函数:

// 标准函数创建协程
func TestRoutine_ShouldSuccess_WhenCreateWithStandardFunction(t *testing.T) {ch := make(chan bool)// func为标准函数go standardFunc(ch)ret := <-chassert.True(t, ret)
}

COPY

执行截图:

file

go+闭包/匿名函数创建协程

这种方式比较方便:

// 闭包/匿名函数创建协程
func TestRoutine_ShouldSuccess_WhenCreateWithAnonymousFunction(t *testing.T) {ch := make(chan bool)// func为闭包/匿名函数go func() {println("Hello, Anonymous Function Go Routine")ch <- true}()ret := <-chassert.True(t, ret)
}

COPY

执行截图:

file

go+方法(struct)创建协程

我们先定义一个struct,struct有一个channel方便我们进行等待协程执行完成:

type s struct {ch chan bool
}

COPY

我们定义两个方法:run和wait,run来执行业务,wait等待run执行完成:

type s struct {ch chan bool
}func (s *s) run() {println("Hello, Struct Method Go Routine")s.ch <- true
}func (s *s) wait() {<-s.ch
}

COPY

我们来创建一个协程执行我们的run方法和wait方法:

func TestRoutine_ShouldSuccess_whenCreateWithStructMethod(t *testing.T) {// 定义struct变量s := &s{ch: make(chan bool),}// 创建协程go s.run()// 等待执行完成s.wait()
}}

COPY

运行截图:

file

interface{}+反射创建协程

我觉得这种方式超级复杂,但是实际业务场景中也特别有用。相当于你可以开发一个调度器,别人提交任务和任务的参数给你,你来控制怎么来调度。
看代码:
我们先定义一个调度函数,参数f是函数,args是f的参数。

func scheduleFunc(wg *sync.WaitGroup, f interface{}, args ...interface{}) {// 通过反射获取函数的定义funcVal := reflect.ValueOf(f)// 然后获取函数的参数// 使用循环把参数加入到slice中in := make([]reflect.Value, len(args))for k, param := range args {in[k] = reflect.ValueOf(param)}wg.Add(1)// 创建调用函数// 我们这儿用匿名函数包装了一下go func() {defer wg.Done() funcVal.Call(in)}()
}

COPY

然后我们定义两个任务函数, task1和task2:

func task1(a string) {fmt.Printf("Hello: %s\n", a)
}func task2(a, b string) {fmt.Printf("Hello: %s-%s\n", a, b)
}

COPY

最后我们来测试一下:

func TestRoutine_ShouldSuccess_whenCreateWithReflect(t *testing.T) {var wg sync.WaitGroup // 创建一个 WaitGroupscheduleFunc(&wg, task1, "Hello, goroutine!")scheduleFunc(&wg, task2, "Hello", "goroutine!")wg.Wait() // 等待所有 goroutine 结束}

COPY

运行截图:

file

package mainimport ("fmt""reflect"
)func worker(data []interface{}) {funcName := data[0].(string)funcArgs := data[1:] // Function or method argumentsfuncValue := reflect.ValueOf(funcMap[funcName])funcArgsValues := make([]reflect.Value, len(funcArgs))for i, arg := range funcArgs {funcArgsValues[i] = reflect.ValueOf(arg)}go funcValue.Call(funcArgsValues)
}var funcMap = map[string]interface{}{"printFunc": printFunc,"printSum":  printSum,
}func printFunc(s string) {fmt.Println(s)
}func printSum(a, b int) {fmt.Println(a + b)
}func main() {worker([]interface{}{"printFunc", "Hello, World!"})worker([]interface{}{"printSum", 1, 2})// Sleep to wait for goroutines to finishfor {}
}

COPY

设置线程和协程的数量对应关系

默认线程数量:

// 获取Go协程使用的线程数量
func TestGPROC_ShouldReturnDefaultNumer_WhenNotSetProcNumber(t *testing.T) {// 如果GOMAXPROCS()的参数为0则是获取线程数量,大于0就是设置线程数量procnum := runtime.GOMAXPROCS(0)fmt.Printf("default proc number: %d\n", procnum)
}

COPY

设置线程数量

使用代码设置线程数需要使用runtime.GOMAXPROCS设置线程数量:

// 获取Go协程使用的线程数量
func TestGPROC_ShouldReturnSpecificNumer_WhenSetProcNumber(t *testing.T) {specnum := 4// 设置线程数量为4,// 如果GOMAXPROCS()的参数为0则是获取线程数量runtime.GOMAXPROCS(specnum)fmt.Printf("set proc number: %d\n", specnum)assert.Equal(t, specnum, runtime.GOMAXPROCS(0))
}

COPY

环境变量设置

在程序启动前设置环境变量GOMAXPROCS就可以了。

export GOMAXPROCS=4

COPY

关闭协程

  1. 自行结束
  2. 手动取消

自行结束

这个和线程类似,协程执行完了就退出了,我们上面的例子都是协程执行完了自动退出。

手动取消

手动取消就需要增加控制机制了,我们来列两个手动取消的例子:

  1. context传递取消信号
  2. channel发送取消信号

我们先来定义一个后台任务,这个后台任务每个一秒钟打印一条:“Hello background task”

// 不用太关注api和语法,只需要知道每个一秒钟打印"Hello background task"
func backgroundTask(ctx context.Context) {ticker := time.NewTicker(1 * time.Second)defer ticker.Stop()for {select {case <-ctx.Done(): // 接收到取消信号,结束 goroutinereturncase <-ticker.C: // 每次 ticker 到时,打印一条消息println("Hello background task")}}
}

COPY

context传递取消信号

直接上代码:

func TestRoutine_ShouldStop_whenSendCancelWithContext(t *testing.T) {ctx, cancel := context.WithCancel(context.Background())go backgroundTask(ctx)// 让 协程 运行一段时间time.Sleep(time.Second * 5)// 发送取消信号cancel()// 给协程留一点时间处理信号time.Sleep(time.Second * 2)
}

COPY

运行截图:

file

channel发送取消信号

直接上代码:

func signaltask(ch chan bool) {for {select {// 接收到取消信号,结束协程case <-ch:return// 没有接收到取消信号,打印一条消息default:println("Hello signal task")time.Sleep(time.Second * 1)}}
}
func TestRoutine_ShouldStop_WhenSendCancelSignal(t *testing.T) {ch := make(chan bool)go signaltask(ch)// 让协程运行5秒钟time.Sleep(time.Second * 5)// 发送取消信号ch <- true// 给协程留一点时间处理信号time.Sleep(time.Second * 2)
}

COPY

运行截图:

file

搞定收工,如有错误请留言告知

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

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

相关文章

数据结构——排序算法

1、排序的概念 排序是指的是将一组数据&#xff08;如数字、单词、记录等&#xff09;按照某种特定的顺序&#xff08;升序或降序&#xff09;进行排列的过程。排序算法是实现排序的程序或方法&#xff0c;它们在软件开发和数据处理中扮演着至关重要的角色。 排序算法可以根据…

servlet开发详解

一、什么是servlet&#xff0c;干什么用的&#xff1f;&#xff1f;&#xff1f; tomcat作为一个web服务器&#xff0c;也称作servlet容器。servlet只有放在web服务器中才能运行&#xff0c;不能独立运行。tomcat这个容器要做三件事&#xff1a;接收请求、处理请求和响应请求。…

文生视频大模型Sora的复现经验

大家好&#xff0c;我是herosunly。985院校硕士毕业&#xff0c;现担任算法研究员一职&#xff0c;热衷于机器学习算法研究与应用。曾获得阿里云天池比赛第一名&#xff0c;CCF比赛第二名&#xff0c;科大讯飞比赛第三名。拥有多项发明专利。对机器学习和深度学习拥有自己独到的…

java调用jacob进行文件转换ppt转pdf或者png

java调用jacob进行文件转换ppt转pdf或者png 前情提要 最近项目上&#xff0c;遇到一个复杂的ppt&#xff0c;最终要求是要将ppt每一页转成图片原本这个是不难&#xff0c;网上一搜一大堆案例&#xff0c;外加我本身也比较精通aspose&#xff0c;那还不是分分钟搞定。结果就是…

Healix Protocol 的 HLX 通证预售:医疗领域的未来展望

Healix Protocol推出 HLX 通证预售&#xff0c;将带来医疗领域的重要变革。通过其区块链技术&#xff0c;Healix Protocol致力于重新定义医疗服务的可及性与负担性&#xff0c;成为医疗行业的希望之光。该项目旨在增强透明度、可及性和效率&#xff0c;推动医疗体系向更加公平和…

Hadoop面试重点

文章目录 1. Hadoop 常用端口号2.Hadoop特点3.Hadoop1.x、2.x、3.x区别 1. Hadoop 常用端口号 hadoop2.xhadoop3.x访问HDFS 端口500709870访问 MR 执行情况端口80888088历史服务器1988819888客户端访问集群端口90008020 2.Hadoop特点 高可靠&#xff1a;Hadoop底层维护多个数…

Rust语言中Regex正则表达式,匹配和查找替换等

官方仓库&#xff1a;https://crates.io/crates/regex 文档地址&#xff1a;regex - Rust github仓库地址&#xff1a;GitHub - rust-lang/regex: An implementation of regular expressions for Rust. This implementation uses finite automata and guarantees linear tim…

LoadBalance 负载均衡服务调用

前身:Ribbon LB负载均衡(Load Balance)是什么 简单的说就是将用户的请求平摊的分配到多个服务上&#xff0c;从而达到系统的HA&#xff08;高可用&#xff09;&#xff0c;常见的负载均衡有软件Nginx&#xff0c;LVS&#xff0c;硬件 F5等 spring-cloud-starter-loadbalancer组…

OSG编程指南<二十一>:OSG视图与相机视点更新设置及OSG宽屏变形

1、概述 什么是视图?在《OpenGL 编程指南》中有下面的比喻,从笔者开始学习图形学就影响深刻,相信对读者学习场景管理也会非常有帮助。 产生目标场景视图的变换过程类似于用相机进行拍照,主要有如下的步骤: (1)把照相机固定在三脚架上,让它对准场景(视图变换)。 (2)…

【办公类-21-11】 20240327三级育婴师 多个二级文件夹的docx合并成docx有页码,转PDF

背景展示&#xff1a;有页码的操作题 背景需求&#xff1a; 实操课终于全部结束了&#xff0c;把考试内容&#xff08;docx&#xff09;都写好了 【办公类-21-10】三级育婴师 视频转文字docx&#xff08;等线小五单倍行距&#xff09;&#xff0c;批量改成“宋体小四、1.5倍行…

洛谷day3

B2053 求一元二次方程 - 洛谷 掌握printf用法&#xff1b; #include <iostream> #include <cmath> using namespace std; double a,b,c; double delta; double x1,x2;int main() {cin>>a>>b>>c;delta b*b-4*a*c;if(delta>0){x1 (-bsqrt…

从根本上优雅地解决 VSCode 中的 Python 模块导入问题

整体概述&#xff1a; 在我尝试运行 test_deal_file.py 时&#xff0c;我遇到了一个 ModuleNotFoundError 错误&#xff0c;Python告诉我找不到名为 controllers 的模块。这意味着我无法从 deal_file.py 中导入 read_excel 函数。 为了解决这个问题&#xff0c;我尝试了几种方法…

JAVA面试大全之JVM和调优篇

目录 1、类加载机制 1.1、类加载的生命周期&#xff1f; 1.2、类加载器的层次? 1.3、Class.forName()和ClassLoader.loadClass()区别? 1.4、JVM有哪些类加载机制&#xff1f; 2、内存结构 2.1、说说JVM内存整体的结构&#xff1f;线程私有还是共享的&#xff1f; 2.2…

Rust使用原始字符串字面量实现Regex双引号嵌套双引号正则匹配

rust使用Regex实现正则匹配的时候&#xff0c;如果想实现匹配双引号&#xff0c;就需要使用原始字符串字面量&#xff0c;不然无法使用双引号嵌套的。r#"..."# 就表示原始字符串字面量。 比如使用双引号匹配&#xff1a; use regex::Regex;fn main() {println!(&qu…

【性能优化】 【回溯】 【字符串】1307. 口算难题

作者推荐 视频算法专题 本文涉及知识点 数学 回溯 字符串 性能优化 LeetCode1307. 口算难题 给你一个方程&#xff0c;左边用 words 表示&#xff0c;右边用 result 表示。 你需要根据以下规则检查方程是否可解&#xff1a; 每个字符都会被解码成一位数字&#xff08;0 - …

2024年云计算使用报告,89%组织用多云,25%广泛使用生成式AI,45%需要跨云数据集成,节省成本是云首要因素

备注&#xff1a;本文来自Flexera2024年的云现状调研报告的翻译。原报告地址&#xff1a; https://info.flexera.com/CM-REPORT-State-of-the-Cloud Flexera是一家专注于做SaaS的IT解决方案公司&#xff0c;有30年发展历史&#xff0c;5万名客户&#xff0c;1300名员工。Flex…

夜莺浏览日志、filebeat采集日志(四)

文章目录 一、elasticsearch二、filebeat三、日志分析 一、elasticsearch docker启动 docker run -d -p 9200:9200 -p 9300:9300 --restartalways -e ES_JAVA_OPTS"-Xms512m -Xmx512m" \ -e discovery.typesingle-node -e xpack.security.enabledtrue -e ELASTIC_P…

使用GO对PostgreSQL进行有意思的多线程压测

前言 针对PostgreSQL进行压缩&#xff0c;有很多相关的工具。有同学又要问了&#xff0c;为何还要再搞一个&#xff1f;比如&#xff0c;pgbench, sysbench之类的&#xff0c;已经很强大了。是的&#xff0c;它们都很强大。但有时候&#xff0c;在一些特殊的场景&#xff0c;可…

Django开发复盘

一、URL 对于一个不会写正则表达式的蒟蒻来说&#xff0c;在urls.py中就只能傻傻的写死名字&#xff0c;但是即便这样&#xff0c;还会有很多相对路径和绝对路径的问题&#xff08;相对ip端口的路径&#xff09;&#xff0c;因为我们网页中涉及到页面跳转&#xff0c;涉及到发送…

Tuxera for Mac2024免费读写硬盘U盘工具

作为软件产品专家&#xff0c;我对各类软件都有较为深入的了解&#xff0c;下面介绍Tuxera for Mac这款读写硬盘/U盘工具的相关信息&#xff1a; Tuxera for Mac是一款高效稳定的NTFS读写工具&#xff0c;专为解决Mac系统无法直接读写NTFS格式驱动器的问题而设计。它提供了完整…