逐步学习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.hqwc.cn/news/572608.html

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

相关文章

前端Web移动端学习day05

移动 Web 第五天 响应式布局方案 媒体查询Bootstrap框架 响应式网页指的是一套代码适配多端&#xff0c;一套代码适配各种大小的屏幕。 共有两种方案可以实现响应式网页&#xff0c;一种是媒体查询&#xff0c;另一种是使用bootstrap框架。 01-媒体查询 基本写法 max-wid…

day70 Mybatis使用mapper重构xml文件重新修改商品管理系统

day67 基于mysql数据库jdbcDruidjar包连接的商品管理用户购物系统-CSDN博客 1多表操作 2动态SQL 项目中使用的为商品管理系统的表 一 查询商品信息 编号&#xff0c;名称&#xff0c;单价&#xff0c;库存&#xff0c;类别 1表&#xff1a;商品表&#xff0c;类别表 n对1…

MySQL—存储引擎和索引

MySQL进阶 1. 存储引擎1.1 体系结构1.2 存储引擎1.3 存储引擎特点1.3.1 InnoDB1.3.2 MyISAM1.3.3 Memory1.3.4 区别及特点 1.4 存储引擎选择 2. 索引2.1 概述2.2 索引结构2.2.1 概述2.2.2 二叉树2.2.3 B-Tree2.2.4 BTree2.2.5 Hash 2.3 索引分类2.3.1 索引分类2.3.2 聚集索引&a…

Axure中后台系统原型模板,B端页面设计实例,高保真高交互54页

作品概况 页面数量&#xff1a;共 50 页&#xff08;长期更新&#xff09; 兼容版本&#xff1a;Axure RP 9/10&#xff0c;不支持低版本 应用领域&#xff1a;网页模板、网站后台、中台系统、B端系统 作品特色 本品为「web中后台系统页面设计实例模板」&#xff0c;默林原创…

libVLC 视频抓图

Windows操作系统提供了多种便捷的截图方式&#xff0c;常见的有以下几种&#xff1a; 全屏截图&#xff1a;通过按下PrtSc键&#xff08;Print Screen&#xff09;&#xff0c;可以截取整个屏幕的内容。截取的图像会保存在剪贴板中&#xff0c;可以通过CtrlV粘贴到图片编辑工具…

律甲法务OA平台:信鸥科技引领法律行业新篇章

随着信息技术的飞速发展&#xff0c;法律行业也迎来了数字化转型的重要时刻。在这个信息化、智能化的时代&#xff0c;如何运用科技手段提升法律服务的质量和效率&#xff0c;成为法律行业亟待解决的问题。信鸥科技&#xff0c;作为业界的佼佼者&#xff0c;凭借其深厚的技术积…

【数据库管理操作】Mysql 创建学生数据库及对数据表进行修改

MySQL 创建学生成绩数据库 1.创建数据库 create database studentscore;创建完成之后&#xff0c;如果需要使用该数据&#xff0c;使用use命令 use studentscore;创建表前查看当前数据库中包含的表 show tables; 2.创建bclass表 create table bclass( class_id char(8) …

数据结构——优先级队列

一、优先级队列的定义和存储 优先级队列定义&#xff1a;优先级高的元素在队头&#xff0c;优先级低的元素在队尾 基于普通线性表实现优先级队列&#xff0c;入队和出队中必有一个时间复杂度O(n),基于二叉树结构实现优先级队列&#xff0c;能够让入队和出队时间复杂度都为O(log…

如何使用Python结合Pillow、matplotlib和OpenCV实现图片读取

使用Pillow库 matplotlib是一个绘图库&#xff0c;经常用于数据可视化&#xff0c;但它也可以用来展示图片。 from PIL import Image# 读取图片 image Image.open(.jpg)# 展示图片 image.show()使用OpenCV库 OpenCV是一个强大的计算机视觉和机器学习库。它不仅提供了大量的图像…

Spring boot2.X 配置https

背景 最近项目组说要将 http 升级成 https 访问&#xff0c;证书也给到我们这边了&#xff0c;当然我们这边用的是个二级域名&#xff0c;采用的是通配符访问的方式&#xff0c;比如一级域名是这样&#xff08;com.chinaunicom.cn&#xff09;&#xff0c;我们的则是&#xff0…

勾八头歌之分类回归聚类

一、机器学习概述 第1关机器学习概述 B AD B BC 第2关常见分类算法 #编码方式encodingutf8from sklearn.neighbors import KNeighborsClassifierdef knn(train_data,train_label,test_data):input:train_data用来训练的数据train_label用来训练的标签test_data用来测试的数据…

Hadoop+Spark大数据技术 第三次作业

第三次作业 1.简述HDFS Shell三种操作命令hadoop fs、hadoop dfs、hdfs dfs的异同点。 相同点 用于与 Hadoop 分布式文件系统&#xff08;HDFS&#xff09;交互。可以执行各种文件系统操作&#xff0c;如文件复制、删除、移动等。 不同点 hadoop fs、hadoop dfs已弃用&#xf…