浅尝Go语言的协程实现

news2024/11/17 10:01:43

文章目录

    • 为什么需要协程
    • 协程的本质
    • 协程如何在线程中执行
    • GMP调度模型
    • 协程并发

为什么需要协程

协程的本质是将一段数据的运行状态进行打包,可以在线程之间调度,所以协程就是在单线程的环境下实现的应用程序级别的并发,就是把本来由操作系统控制的切换+保存状态在应用程序里面实现了。

所以我们需要协程的目的其实就是它更加节省资源、可以在有限的资源内支持更高的并发,体现在以下三个方面:

  • 资源利用:程可以利用任何的线程去运行,不需要等待CPU的调度。
  • 快速调度:协程可以快速地调度(避开了系统调用和切换),快速的切换。
  • 超高并发:有限的线程就可以并发很多的协程。

协程的本质

协程在go语言中使用runtime\runtime2.go下的g结构体来表示,这个结构体中包含了协程的很多信息,我们只挑选其中的重要字段来进行分析:

type g struct {
	// 协程的栈帧,里面包含了两个字段:lo和hi,分别是协程栈的高位指针和低位指针
	stack       stack
	// gobuf结构体中储存了很多与协程栈相关的指针,比如pc、sp
	sched     gobuf
	// 用来标记协程当前的状态
	atomicstatus uint32
	// 每个协程的唯一标识,不向应用层暴露。但是goid的地址会存在寄存器里面,可以通过ebpf工具无侵入地去获取
	goid         int64
}

在这里插入图片描述

对线程的描述

我们知道,go语言中的协程是跑在线程上面的,那么go中肯定会有对线程的抽象描述,这个结构体也在runtime\runtime2.go中,我们只展示重要的部分:

type m struct {
	// 每次启动一个M都会第一个创建的gourtine,用于操作调度器,所以它不指向任何函数,只负责调度
	g0      *g     // goroutine with scheduling stack
	// 当前正在线程上运行的协程
	curg          *g       // current running goroutine
	// 线程id
	id            int64
	// 记录每种操作系统对于线程额外的描述信息
	mOS
}

协程如何在线程中执行

我们从最简单的单线程调度模型来看,协程在线程中的执行流程可以参考下图:

在这里插入图片描述

线程循环

在go中每个线程都是循环执行一系列工作,又称作单线程循环如下图所示:左侧为栈,右侧为线程执行的函数顺序,其中的业务方法就是协程方法。

普通协程栈只能记录业务方法的业务信息,且当线程没有获得协程之前是没有普通协程栈的。所以在内存中开辟了一个g0栈,专门用于记录函数调用跳转的信息,因此g0栈其实就是调度中心的栈。

线程循环会按顺序循环去执行上图右侧的函数:schedule->execute->gogo->业务方法->goexit

schedule

schedule函数的作用是为当前的P获取一个可以执行的g,并执行它。

  1. 首先会有1/61的概率检查全局队列,确保全局队列中的G也会被调度。
  2. 然后有60/61的概率从本地队列中获取g。
  3. 如果从本地队列中没有获取到可执行的g,就会调用findrunnable函数去获取。
    findrunnable函数的流程:
    1. 调用runqget函数来从P自己的runnable G队列中得到一个可以执行的G;
    2. 如果1失败,调用globrunqget函数从全局runnableG队列中得到一个可以执行的G;
    3. 如果2失败,调用netpoll(非阻塞)函数取一个异步回调的G;
    4. 如果3失败,尝试从其他P那里偷取一半数量的G过来;
    5. 如果4失败,再次调用globrunqget函数从全局runnableG队列中得到一个可以执行的G;
    6. 如果5失败,调用netpoll(阻塞)函数取一个异步回调的G;
    7. 如果6仍然没有取到G,那么调用stopm函数停止这个M。
  4. 如果获取到了可执行的g,就调用execute函数去执行。
// One round of scheduler: find a runnable goroutine and execute it.
// Never returns.
func schedule() {
	......
	// 新建一个gp变量,gp就是即将要运行的协程指针
	var gp *g
	var inheritTime bool

	// 垃圾回收相关的工作
	......
	
	// 调度过程中有1/61的概率检查全局队列,确保全局队列中的G也会被调度。
	// M绑定的P首先有1/61概率从全局队列获取G,60/61概率从本地队列获取G
	if gp == nil {
		// Check the global runnable queue once in a while to ensure fairness.
		// Otherwise two goroutines can completely occupy the local runqueue
		// by constantly respawning each other.
		if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
			lock(&sched.lock)
			gp = globrunqget(_g_.m.p.ptr(), 1)
			unlock(&sched.lock)
		}
	}
	// 从本地队列中获取g
	if gp == nil {
		gp, inheritTime = runqget(_g_.m.p.ptr())
		// We can see gp != nil here even if the M is spinning,
		// if checkTimers added a local goroutine via goready.
	}
	// 如果从本地队列获取失败,就会调用findrunnable函数去获取g
	if gp == nil {
		gp, inheritTime = findrunnable() // blocks until work is available
	}
	......
	execute(gp, inheritTime)
}

execute

execute函数会为schedule获取到的可执行协程初始化相关结构体,然后以sched结构体为参数调用gogo函数:

func execute(gp *g, inheritTime bool) {
	_g_ := getg()

	// 初始化g结构体
	// Assign gp.m before entering _Grunning so running Gs have an
	// M.
	_g_.m.curg = gp
	gp.m = _g_.m
	casgstatus(gp, _Grunnable, _Grunning)
	gp.waitsince = 0
	gp.preempt = false
	gp.stackguard0 = gp.stack.lo + _StackGuard
	if !inheritTime {
		_g_.m.p.ptr().schedtick++
	}
	
	......
	// 汇编实现的函数,通过gobuf结构体中的信息,跳转到执行业务的方法
	gogo(&gp.sched)

gogo

gogo函数实际上是汇编实现的,每个操作系统实现的gogo方法是不同的,它会通过传进来的gobuf结构体,先向普通协程栈中压入goexit函数,然后跳转到执行业务的方法,协程栈也会被切换成业务协程自己的栈。

业务方法

业务方法就是协程中需要执行的相关函数。

goexit

goexit也是汇编实现的,当执行完协程栈中的业务方法之后,就会退到goexit方法中,它会将业务协程的栈切换成调度器的栈(也就是g0栈),然后重新调用schedule函数,形成一个闭环。

GMP调度模型

上述的调度模型是单线程的,但是现代CPU往往是多核的,应用采用的也是多线程,因此单线程调度模型有些浪费资源。所以我们在实际使用中,其实是一种多线程循环。但是多个线程在获取可执行g的时候就会存在并发冲突的问题,所以就有了GMP调度模型。

GMP调度模型简单来说是这样的:

G是指协程goroutine,M是指操作系统线程,P是指调度器。

首先,GMP调度模型中有一个全局队列,用于存放等待运行的G。然后每个P都有自己的本地队列,存放的也是等待运行的G,但是存的数量有限,不会超过256个。我们新建goroutine的时候,是优先放到P的本地队列中的,如果队列满了,会把本地队列中一半的G都移到全局队列中。

线程想运行任务就得获取P,从P的本地队列获取G,G执行之后,M会从P获取下一个G,不断重复下去。P队列为空时,M会尝试从全局队列拿一批G放到P的本地队列,如果获取不到就会从其他P的本地队列偷一半放到自己P的本地队列。

当M执行某一个G时候如果发生了系统调用或者其余阻塞操作,M会阻塞,如果当前有一些G在执行,runtime会把这个线程M从P中摘除(detach),然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P。当M系统调用结束时候,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列。如果获取不到P,那么这个线程M变成休眠状态, 加入到空闲线程中,然后这个G会被放入全局队列中。

P的底层结构

我们发现GMP调度模型中有一个P,P就是调度器,我们来看一下P的底层数据结构,同样在runtime\runtime2.go文件中:

type p struct {
	id          int32
	status      uint32 // one of pidle/prunning/...
	// 指向调度器服务的那个线程
	m           muintptr   // back-link to associated m (nil if idle)

	// Queue of runnable goroutines. Accessed without lock.
	// 调度器的本地队列,因为只服务于一个线程,所以可以无锁的访问,队列本身实际上是一个大小为256的指针数组
	runqhead uint32
	runqtail uint32
	runq     [256]guintptr
	// 指向下一个可用g的指针
	runnext guintptr
}

协程并发

我们上面介绍的调度模型实际上是非抢占式的,非抢占式模型的特点就是只有当协程主动让出后,M才会去运行本地队列后面的协程,那么这样就很容易造成队列尾部的协程饿死。

其实Go语言的协程是基于抢占式来实现的,也就是当协程执行一段时间后将当前任务暂定,执行后续协程任务,防止时间敏感携程执行失败。如下图所示:

在这里插入图片描述
抢占式调度

当目前线程中执行的协程是一个超长时间的任务,此时先保存该协程的运行状态也就是保护现场,若是后续还需继续执行就将其放入本地队列中去,如果不需要执行就将其处于休眠状态,然后直接跳转到schedule函数中。

实现:

  1. 主动挂取:gopark方法,当业务调用这个方法线程就会直接回到schedule函数并切换协程栈,当前运行的协程将会处于等待状态,等待状态的协程是无法立即进入任务队列中的。程序员无法主动调用gopark函数,但是我们可以通过Sleep等具有gopark的函数来进行主动挂取,Sleep五秒之后系统将会把任务的等待状态更改为运行状态放入队列中。
  2. 系统调用完成时:go程序在运行状态中进行了系统调用,那么当系统的底层调用完成后就会调用exitsyscall函数,线程就会停止执行当前协程,将当前协程放入队列中去。
  3. 标记抢占morestack():当函数跳转时都会调用这个方法,它的本意在于检查当前协程栈空间是否有足够内存,如果不够就要扩大该栈空间。当系统监控到协程运行超过10ms,就将g.stackguard0置为0xfffffade(该值是一个抢占标志),让程序在只执行morestack函数时顺便判断一下是否将g中的stackguard置为抢占,如果的确被标记抢占,就回到schedule方法,并将当前协程放回队列中。

全局队列的饥饿问题

上述操作让本地队列成了一个小循环,但是如果目前系统中的线程的本地队列中都拥有一个超大的协程任务,那么所有的线程都将在一段时间内处于忙碌状态,全局队列中的任务将会长期无法运行,这个问题又称为全局队列饥饿问题,解决方式就是在本地队列循环时,以一定的概率从全局队列中取出某个任务,让它也参与到本地循环当中去。

其实在执行schedule函数寻找可运行g的时候,首先会去执行下面的代码,即调度过程中有1/61的概率去全局队列中获取可执行的协程,防止全局队列中的协程被饿死。

	// 调度过程中有1/61的概率检查全局队列,确保全局队列中的G也会被调度。
	if gp == nil {
		// Check the global runnable queue once in a while to ensure fairness.
		// Otherwise two goroutines can completely occupy the local runqueue
		// by constantly respawning each other.
		if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
			lock(&sched.lock)
			gp = globrunqget(_g_.m.p.ptr(), 1)
			unlock(&sched.lock)
		}
	}

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

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

相关文章

微服务框架 SpringCloud微服务架构 25 黑马旅游案例 25.1 搜索、分页

微服务框架 【SpringCloudRabbitMQDockerRedis搜索分布式,系统详解springcloud微服务技术栈课程|黑马程序员Java微服务】 SpringCloud微服务架构 文章目录微服务框架SpringCloud微服务架构25 黑马旅游案例25.1 搜索、分页25.1.1 直接开干25 黑马旅游案例 25.1 搜…

PyTorch~自定义数据读取

这次是PyTorch的自定义数据读取pipeline模板和相关trciks以及如何优化数据读取的pipeline等。 因为有torch也放人工智能模块了~ 从PyTorch的数据对象类Dataset开始。Dataset在PyTorch中的模块位于utils.data下。 from torch.utils.data import Dataset围绕Dataset对象分别从…

前端入门必备基础

化繁为简 HTML5要的就是简单、避免不必要的复杂性。HTML5的口号是“简单至上,尽可能简化”。因此,HTML5做了以下改进: 以浏览器原生能力替代复杂的JavaScript代码。 新的简化的DOCTYPE。 新的简化的字符集声明。 简单而强大的HTML5API。…

[附源码]Python计算机毕业设计SSM基于云数据库的便民民宿租赁系统(程序+LW)

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

《Linux运维实战:MongoDB数据库全量逻辑备份恢复(方案一)》

一、备份与恢复方案 mongodump是MongoDB官方提供的备份工具,它可以从MongoDB数据库读取数据,并生成BSON文件,mongodump适合用于备份和恢复数据量较小的MongoDB数据库, 不适用于大数据量备份。 默认情况下mongodump不获取local数据库里面的内容。mongodump仅备份数据库中的文档&…

回溯算法(1)组合

文章目录回溯算法理论77. 组合216. 组合总和17. 电话号码的组合回溯算法理论 回溯算法其实就是递归,只不过递归又分为递去和归来,其中归来便就是回溯。 为什么要使用回溯? 有些问题我们通过暴力解法也很难解决,比如说我们接下来…

C语言学习之路(高级篇)—— 变量和内存分布(上)

说明:该篇博客是博主一字一码编写的,实属不易,请尊重原创,谢谢大家! 数据类型 1) 数据类型概念 什么是数据类型?为什么需要数据类型? 数据类型是为了更好进行内存的管理,让编译器能确定分配…

04 | 云硬盘的使用方法

前期环境: Ubuntu 0 云硬盘类型 云硬盘类型包括: 高性能云硬盘通用型 SSD 云硬盘SSD 云硬盘增强型 SSD 云硬盘极速型 SSD 云硬盘,仅支持随存储增强型云服务器一同购买,不支持单独购买 1 创建云硬盘 1.1 创建方式 1.1.1 单个…

第二证券|连拉20CM涨停!防疫新概念股火了!恒生科技指数涨逾5%

周四上午,“新十条”发布后,由于A股商场已反弹一段时刻,两市股指今天早盘接连震动走势,港股在地产、科技、消费等板块带动下,体现更为强势。 A股上证指数早盘在3200点附近持续震动,光伏、化肥、物流、港口等…

JavaScript内置对象(内置对象、查文档(MDN)、Math对象、日期对象、数组对象、字符串对象)

目录 JavaScript内置对象 内置对象 查文档 MDN Math对象 Math概述 案例一:封装自己的对象 随机数方法 random() 案例一:猜数字游戏 日期对象 Date 概述 Date()方法的使用 获取日期的总的毫秒形式 案例一:倒计时效果 数组对象 …

DoltLab本地部署实践

目录引言Dolt是什么?如何本地部署使用DoltLab具体安装步骤安装期间FAQ写在最后其他相关资料引言 自从搞深度学习训练模型以来,一直有个问题困扰着我:训练所用数据集的管理。为什么说这是一个问题呢? 在读研时,我们依据…

ELK日志分析系统概述及部署

文章目录一、ELK日志分析系统1、概念2、完整日志系统基本特征3、使用ELK的原因4、ELK 的工作原理二、ELK日志分析系统集群部署的操作步骤环境准备:1、 ELK Elasticsearch 集群部署(在Node1、Node2节点上操作)1.1、更改主机名、配置域名解析、…

剑指 Offer 53 - I. 在排序数组中查找数字 I

摘要 剑指 Offer 53 - I. 在排序数组中查找数字 I 一、二分查找 1.1 二分查找的分析 由于数组已经排序,因此整个数组是单调递增的,我们可以利用二分法来加速查找的过程。 考虑 target在数组中出现的次数,其实我们要找的就是数组中「第一…

汇编语言ch2_2 汇编语言中的debug

使用debug 可以完成以下功能: 可以查看 和改变 CPU 中,寄存器的内容;可以查看 和改变内存中的内容;可以将内存中的 机器指令 翻译成汇编指令使用汇编指令 在 内存中 存入 机器指令执行机器指令 首先,启动 Debug,在DO…

实现数智内控,数据分析创造价值——辽宁烟草智能风险体检系统

近两年,烟草行业部分单位围绕中心任务,结合实际,守正创新,开展了许多研究探索。比如,在财务大数据价值挖掘、会计共享中心建设、财务风险预警系统建设等方面做了大量卓有成效的工作。在这样的背景下,辽宁烟…

DSPE-MAL 磷脂改性马来酰亚胺简介CAS1360858-99-6

DSPE-MAL二硬脂酰磷脂酰乙醇胺改性马来酰亚胺 中文名称:二硬脂酰磷脂酰乙醇胺改性马来酰亚胺 英文名称:DSPE-MAL CAS:1235864-97-7 分子式:C48H86N2NaO11P 分子量:921.16700 外观:白色粉末 DSPE-MAL二…

2022icpc 济南站 持续补题

链接:Dashboard - 2022 International Collegiate Programming Contest, Jinan Site - Codeforces 签到题:k K. Stack Sort You are given a permutation with nn numbers, a1,a2,…,an(1≤ai≤n,ai≠aj when i≠j). You want to sort these numbers …

WY易盾cb、fp逆向分析

内容仅供参考学习 欢迎朋友们V一起交流: zcxl7_7 目标 网址:案例地址 这个好像还没改版,我看官网体验那边已经进行了混淆 分析 这个进行的请求很乱,我就不说怎么找的了,到时候越听越乱。一共有2个请求很重要 …

笔试题之编写SQL按要求查询用户阅读行为数据

紧张源于恐惧,恐惧源于未知。 文章目录前言一、SQL题目二、当时作答结果三、复盘(一)建表并自定义插入数据(二)正确解答(三)答错原因分析总结前言 分享本人一次失败的笔试经历,供各…

plink中的BGEN格式的数据如何用

这里,介绍一下BGEN格式的数据,他的文件格式是这样的:a.bgen,这是一个新的数据格式,目前应用不如plink的二进制文件:.bim,.bed,.fam。这里介绍一下如何相互转换。 1. bgen格式介绍 现代遗传关联研究通常使…