逐步学习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 := <-ch
    assert.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 := <-ch
    assert.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 // 创建一个 WaitGroup

    scheduleFunc(&wg, task1, "Hello, goroutine!")
    scheduleFunc(&wg, task2, "Hello", "goroutine!")

    wg.Wait() // 等待所有 goroutine 结束

}

COPY

运行截图:

file

package main

import (
    "fmt"
    "reflect"
)

func worker(data []interface{}) {
    funcName := data[0].(string)
    funcArgs := data[1:] // Function or method arguments

    funcValue := 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 finish
    for {}
}

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(): // 接收到取消信号,结束 goroutine
            return
        case <-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

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

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

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

相关文章

每日一题--- 环形链表[力扣][Go]

环形链表 题目&#xff1a;142. 环形链表 II 给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回 null。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给…

数字身份的革命:解锁 Web3 的身份验证技术

引言 随着数字化时代的到来&#xff0c;个人身份认证成为了日常生活和商业活动中不可或缺的一部分。传统的身份验证方式存在着安全性低、易伪造、不便利等问题&#xff0c;因此&#xff0c;人们迫切需要一种更安全、更便捷的身份验证技术。在这样的背景下&#xff0c;Web3的身…

数仓建设实践——58用户画像数仓建设

目录 一、数据仓库&用户画像简介 1.1 数据仓库简介 1.2 数据仓库的价值 1.3 用户画像简介 1.4 用户画像—标签体系 二、用户画像数仓建设过程 2.1 画像数仓—背景&现状 2.2 画像数仓—整体架构 2.3 画像数仓—研发流程 2.4 画像数仓—指标定义 2.5 画像数仓…

Java基本数据结构(基于jdk11)

java中有很多数据类型&#xff0c;以下数据类型都出于java.util包下且日常经常使用的&#xff0c;先介绍一下接口&#xff0c;接口可以很快的了解到这个数据结构的特性。 接口 List: 有序队列&#xff0c;如&#xff1a;ArrayList、LinkedList Deque&#xff1a;双端队列&am…

视图的作用

目录 视图的作用 创建视图 为 scott 分配创建视图的权限 查询视图 复杂视图的创建 视图更新的限制问题 更新视图中数据的部门编号&#xff08;视图的存在条件&#xff09; 限制通过视图修改数据表内容 创建只读的视图 复杂视图创建 oracle从入门到总裁:​​​​​​h…

Android 性能优化(六):启动优化的详细流程

书接上文&#xff0c;Android 性能优化&#xff08;一&#xff09;&#xff1a;闪退、卡顿、耗电、APK 从用户体验角度有四个性能优化方向&#xff1a; 追求稳定&#xff0c;防止崩溃追求流畅&#xff0c;防止卡顿追求续航&#xff0c;防止耗损追求精简&#xff0c;防止臃肿 …

【研发日记】Matlab/Simulink开箱报告(十)——Signal Routing模块模块

文章目录 前言 Signal Routing模块 虚拟模块和虚拟信号 Mux和Demux Vector Concatenate和Selector Bus Creator和Bus Selector 分析和应用 总结 前言 见《开箱报告&#xff0c;Simulink Toolbox库模块使用指南&#xff08;五&#xff09;——S-Fuction模块(C MEX S-Fun…

SQLite中的动态内存分配(五)

返回&#xff1a;SQLite—系列文章目录 上一篇&#xff1a;SQLite中的原子提交&#xff08;四&#xff09; 下一篇&#xff1a;SQLite使用的临时文件&#xff08;二&#xff09; ​概述 SQLite使用动态内存分配来获得 用于存储各种对象的内存 &#xff08;例如&#xff1a…

android 11 SystemUI 状态栏打开之后的界面层级关系说明之一

比如WiFi 图标的父layout为&#xff1a; Class Name: ButtonRelativeLayout Class Name: QSTileView Class Name: TilePage Class Name: PagedTileLayout Class Name: QSPanel Class Name: NonInterceptingScrollView Class Name: QSContainerImpl Class Name: FrameLayout Cl…

2018年亚马逊云科技推出基于Arm的定制芯片实例

2018年&#xff0c;亚马逊云技术推出了基于Arm的定制芯片。 据相关数据显示&#xff0c;基于Arm的性价比比基于x86的同类实例高出40%。 这打破了对 x86 的依赖&#xff0c;开创了架构的新时代&#xff0c;现在能够支持多种配置的密集计算任务。 这些举措为亚马逊云技术的其他创…

计算机票.java

题目&#xff1a;机票价格按照淡季旺季&#xff0c;头等舱和经济舱收费&#xff0c;输入机票原价&#xff0c;月份&#xff0c;头等舱或经济舱 。按照如下规则计算机票价格&#xff1a;旺季&#xff08;5-10月&#xff09;头等舱九折&#xff0c;经济舱8.5折&#xff0c;淡季&a…

Redis中的客户端(二)

客户端 输入缓冲区。 客户端状态的输入缓冲区用于保存客户端发送的命令请求: typedef struct redisClient {// ...sds querybuf;// ... }redisClient;例子 举个例子&#xff0c;如果客户端向服务器发送了以下命令请求: SET key value那么客户端状态的qureybuf属性将是一个…

帆软报表在arm架构的linux

有朋友遇到一个问题在部署帆软报表时遇到报错。 问 我在 arm架构的linux服务器上部署帆软报表遇到了一个棘手的问题&#xff0c;你有空帮忙看下嘛。 我看后台日志报的错是 需要升级 gcc、libmawt.so &#xff0c;是系统中缺少Tomcat需要的依赖库&#xff0c;你之前处理过类似…

OCP NVME SSD规范解读-15.DSSD set feature功能要求-2

启用IEEE1667隔离区(Enable IEEE1667 Silo)&#xff1a;特征标识符C4h允许开启符合IEEE1667标准的安全存储区功能&#xff0c;以实现数据的隔离和安全存储。 4.15.9章节描述了启用IEEE1667 Silo&#xff08;通过Feature Identifier C4h标识的Set Feature命令&#xff09;的相关…

高级DBA带你处理MySQL客户端程序频繁访问MYSQL数据库并错误链接不释放导致连接数爆满事故实战

高级DBA带你处理MySQL客户端程序频繁访问MYSQL数据库并错误链接不释放导致连接数爆满事故实战 一、生产事故描述 Mysql生产数据库最大连接数爆满&#xff0c;其余客户端也同样拿不到数据库连接&#xff0c;生产异常&#xff0c;数据传输失败&#xff01; 报错如下&#xff1a…

iOS17 隐私协议适配详解

1. 背景 网上搜了很多文章&#xff0c;总算有点头绪了。其实隐私清单最后做出来就是一个plist文件。找了几个常用三方已经配好的看了看&#xff0c;比着做就好了。 WWDC23 中关于隐私部分的更新&#xff08;WWDC23 隐私更新官网&#xff09;&#xff0c;其中提到了第三方 SDK 的…

Fastjson配置消息转换器(时间格式问题)

问题&#xff1a; 我们可以看见&#xff0c;日期的格式有点问题。 由于ArticleListVO类的createTime成员变量是Date类型&#xff0c;默认是由java的Jackson来处理&#xff0c;使用 ISO-8601 规范来处理日期时间格式。ISO-8601 是一种国际标准的日期时间表示法&#xff0c;例如&…

基于R语言的DICE模型技术应用

随着温室气体排放量的增大和温室效应的增强&#xff0c;全球气候变化问题受到日益的关注。我国政府庄严承诺在2030和2060年分别达到“碳达峰”和“碳中和”&#xff0c;因此气候变化和碳排放已经成为科研人员重点关心的问题之一。气候变化问题不仅仅是科学的问题&#xff0c;同…

6_相机坐标系_相机4个坐标系详述

相机系列文章是用来记录使用opencv3来完成单目相机和6轴机械臂手眼标定。本人吃饭的主职是linux下6轴机械臂相关应用开发。但对于机械臂运动学、相机应用等都非常感兴趣&#xff0c;所以对一些线性代数基础薄弱又想深入了解机械臂内部运算的同志比较有体会。由于是探索性学习&a…

ssm小区车库停车系统开发mysql数据库web结构java编程计算机网页源码eclipse项目

一、源码特点 ssm小区车库停车系统是一套完善的信息系统&#xff0c;结合springMVC框架完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用SSM框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模…