go协程、线程的本质,如何协调运作

这篇具有很好参考价值的文章主要介绍了go协程、线程的本质,如何协调运作。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

协程与线程

线程在创建、切换、销毁时候,需要消耗CPU的资源。

协程就是将一段程序的运行状态打包, 可以在线程之间调度。减少CPU在操作线程的消耗

协程、线程、进程 这块网上非常多文章讲了,就不多叙述了。

归纳下:

进程用分配内存空间
线程用来分配CPU时间
协程用来精细利用线程
协程的本质是一段包含了运行状态的程序  后面介绍后,会对这个概念更好理解

协程的本质

上面讲了 ,协程的本质就是 一段程序的运行状态的打包:

  func Do() {
  	for i := 1; i <= 1000; i++ {
  		fmt.Println(i)
  		time.Sleep(time.Second)
  	}
  }

  func main() {
  	go Do()
  	select {}
  }

例如上面这段代码,开了一个协程,然后一直循环打印。

假设程序都还有很多其他的协程也在工作,发现这个协程工作太久了,系统会进行切换别的协程,现在这个协程会放入协程队列中。

问题:要做到这点,协程需要怎么保存这个执行状态?

  1. 需要一个函数的调用栈,记录执行了那些函数,(例子中只有一个,正常情况下会是很多函数相互调用) 函数执行完后,还需要回到上层函数,所以要保存函数栈信息。
  1. 需要记录当前执行到了 那行代码,不能把多执行,也不能少执行那句代码,不然程序会不可控。
  1. 需要一个空间,存储整个协程的数据,例如变量的值等。

协程的底层定义

在runtime的runtim2.go中

  type g struct {
  // 只留了少量几个,里面有非常多的字段。
  	stack       stack  // 调用栈
  	m         *m        // 协程关联了一个m (GMP)
        sched     gobuf  // 协程的现场
  	goid         uint64   // 协程的编号
      atomicstatus atomic.Uint32 // 协程的状态

  }

type gobuf struct {
       sp   uintptr  // 当前调用的函数
	pc   uintptr  // 执行语句的指针
	g    guintptr
	ctxt unsafe.Pointer
	ret  uintptr
	lr   uintptr
	bp   uintptr // for framepointer-enabled architectures
}

// 栈的定义
  type stack struct {
  	lo uintptr  // 低地址
  	hi uintptr  // 高地址
  }

整体下:

假如有这么一段代码:

  func do3() {
  	fmt.Println("dododo")
  }

  func do2() {
  	do3()
  }

  func do1() {
  	do2()
  }

  func main() {
  	go do1()
  	time.Sleep(time.Hour)
  }

在do2断点:

go协程、线程的本质,如何协调运作

能看到下方的调用栈中,会自动插入一个 goexit 在栈头。

小结下,整体的结构如下:

go协程、线程的本质,如何协调运作

总结:

runtime 中,协程的本质是一个g 结构体
stack:堆栈地址
gobuf:目前程序运行现场
atomicstatus: 协程状态

线程的底层 m

操作系统的线程是由操作系统管理,这里的m只是记录线程的信息。

截取部分代码:
type m struct {
	g0      *g     // goroutine with scheduling stack
	id            int64 // id号
	morebuf gobuf  // gobuf arg to morestack	
	curg          *g       // 当前运行的g
	p             puintptr // attached p for executing go code (nil if not executing go code)
	mOS // 系统线程信息
}

go 是go程序启动创建的第一个协程,用来操控调度器的,第二个是主协程,可以看下 go启动那篇

小结:

runtime 中将操作系统线程抽象为 m结构体
g0:g0协程,操作调度器
curg:current g,目前线程运行的g
mOs:操作系统线程信息

如何工作

协程究竟是如何在 线程中工作的 ?

先讲总结,然后跟着总结往下看:

go协程、线程的本质,如何协调运作

这是单个线程的循环,没有P的存在。

1. schedule() 是线程获取 协程的入口方法

线程通过执行 g0协程栈,获取 待执行的 协程

也就是意味着,每次线程执行 这个schedule方法,就意味着会切换一个 协程。
这个结论很重要,后面 协程调度时候,会大量看到调用这个方法。

在runtime的 proc.go下面能看到这个方法,这里只留了两行代码,
只和目前逻辑相关的,这个方法后面还要多次读
   func schedule() {
  	gp, inheritTime, tryWakeP := findRunnable() // blocks until work is available
  	execute(gp, inheritTime)
  }
这里的gp就是 待执行的g

 可以和上面的图对上,这里去  `Runnable` 找一个协程。然后,调用 `execute` 方法。
 至于怎么去找的,知道GMP的肯定都知道,这个后面聊。

也只有部分代码,和这里业务相关的
 func execute(gp *g, inheritTime bool) {
  	mp := getg().m  //获取m,线程的抽象
  	mp.curg = gp   // 还记得 m的定义 里面有个 当前的 g 在这里赋值了
  	gp.m = mp      // g的定义也有个 m,这里也赋值了
  	gogo(&gp.sched)
  }

到gogo
func gogo(buf *gobuf) // 只有定义,说明是汇编实现的,而且是平台相关的
     
// func gogo(buf *gobuf)  
// 这里把 g的gobuf传过去了,gobuf 存着 sp 和 pc ,当前的执行函数,和执行语句
//  到这里就基本对应上了
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $0-8
	MOVQ	buf+0(FP), BX		// gobuf
	MOVQ	gobuf_g(BX), DX
	MOVQ	0(DX), CX		// make sure g != nil
	JMP	gogo<>(SB)

//  插入了 goexit的栈针 然后开始运行业务 
TEXT gogo<>(SB), NOSPLIT, $0
	get_tls(CX)
	MOVQ	DX, g(CX)
	MOVQ	DX, R14		// set the g register
	MOVQ	gobuf_sp(BX), SP	// restore SP 插入了 goexit的栈针
	MOVQ	gobuf_ret(BX), AX
	MOVQ	gobuf_ctxt(BX), DX
	MOVQ	gobuf_bp(BX), BP
	MOVQ	$0, gobuf_sp(BX)	// clear to help garbage collector
	MOVQ	$0, gobuf_ret(BX)
	MOVQ	$0, gobuf_ctxt(BX)
	MOVQ	$0, gobuf_bp(BX)
	MOVQ	gobuf_pc(BX), BX
	JMP	BX

在运行业务之前 jmp bx,都还在 g0的协程栈上。

目前,已经把开始执行,到执行都整理了一遍,但是,没有讲 goexit 插入 到底有什么作用?

经验丰富的伙伴大致能猜到, 当执行完了协程的任务后,需要回到 schedule方法中, 线程重新去执行别的协程,这就是 goexit的作用

goexit

汇编实现
TEXT runtime·goexit(SB),NOSPLIT|TOPFRAME,$0-0
BYTE	$0x90	// NOP
CALL	runtime·goexit1(SB)	//  去调用 goexit1 这个方法

// Finishes execution of the current goroutine.
func goexit1() {
	mcall(goexit0) // 通过mcall 调用goexit0  
}

 // mcall switches from the g to the g0 stack and invokes fn(g),
 // 切换到 g0 栈
 func mcall(fn func(*g))
 就是只,上面的都是在 业务协程中,运行的,到这里,开始使用 g0栈去运行,goexit0

// goexit continuation on g0. 
func goexit0(gp *g) {
	mp := getg().m
	pp := mp.p.ptr()

	casgstatus(gp, _Grunning, _Gdead)
	gcController.addScannableStack(pp, -int64(gp.stack.hi-gp.stack.lo))
	if isSystemGoroutine(gp, false) {
		sched.ngsys.Add(-1)
	}
	gp.m = nil
	locked := gp.lockedm != 0
	gp.lockedm = 0
	mp.lockedg = 0
	gp.preemptStop = false
	gp.paniconfault = false
	gp._defer = nil // should be true already but just in case.
	gp._panic = nil // non-nil for Goexit during panic. points at stack-allocated data.
	gp.writebuf = nil
	gp.waitreason = waitReasonZero
	gp.param = nil
	gp.labels = nil
	gp.timer = nil
	schedule()
}
// 对结束的g进行了一些置0的工作,然后调用了 schedule()

schedule() 意味着 为现在的线程,切换协程。

到此,和上面的图都对应上了。但是目前还是单线程,多线程时候,是如何工作了,下篇再聊。文章来源地址https://www.toymoban.com/news/detail-747829.html

到了这里,关于go协程、线程的本质,如何协调运作的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 港大&谷歌提出GO-NeRF:在NeRF中生成协调且高质量的3D对象

    尽管在3D生成方面取得了进展,但在作为NeRF表示的现有3D场景中直接创建3D对象仍然是未经探索的。这个过程不仅需要高质量的3D对象生成,还需要将生成的3D内容无缝地合成到现有的NeRF中。为此,作者提出了一种新方法,GO-NeRF,能够利用场景上下文进行高质量和谐调的3D对象

    2024年01月18日
    浏览(33)
  • 详解区块链技术,如何运作

    区块链是数字加密货币比特币的核心技术。区块链是一个分布式数据库,包含参与方之间已执行和共享的所有交易或数字事件的记录。每一笔交易都经过系统大多数参与者的验证。它包含每笔交易的每一条记录。比特币是最流行的加密货币,也是区块链的一个例子。区块链技

    2023年04月15日
    浏览(28)
  • python正则+多线程(代理)+线程池+协程

    常用元字符 . 匹配除换行符以外的任意字符 w 匹配字幕或数字或下划线 s 匹配任意空白字符 d 匹配数字 n 匹配一个换行符 t 匹配一个制表符 W 匹配非字母或数字或下划线 D 匹配非数字 S 匹配非空白符 a|b 匹配字符a或b () 匹配括号内的表达式,也表示一个组 […] 匹配字符

    2024年02月16日
    浏览(30)
  • Go 方法介绍,理解“方法”的本质

    目录 Go 方法介绍,理解“方法”的本质 一、认识 Go 方法 1.1 基本介绍 1.2 声明 1.2.1 引入 1.2.2 一般声明形式 1.2.3 receiver 参数作用域 1.2.4 receiver 参数的基类型约束 1.2.5 方法声明的位置约束 1.2.6 如何使用方法 二、方法的本质 三、巧解难题 我们知道,Go 语言从设计伊始,就不支

    2024年02月06日
    浏览(31)
  • go-GMP 协程切换时机 和 协程实战

    当m在执行某个g的时候,g非常耗时,例如一个for循环,每次循环sleep1分钟,循环1000次。 这个例子看似无聊,却是很难解决的,成功的避开了2个系统切换时机。 如果这个时候,一直执行这个g,别的g就会得不到执行,例如有g是处理用户支付的,这样就会造成收钱不积极。 本

    2024年02月05日
    浏览(35)
  • 【Go 基础篇】Go语言字符类型:解析字符的本质与应用

    字符类型是计算机编程中用于表示文本和字符的数据类型,是构建字符串的基本单位。在Go语言(Golang)中,字符类型具有独特的特点和表示方式,包括Unicode编码、字符字面值以及字符操作。本篇博客将深入探讨Go语言中的字符类型,介绍字符的编码方式、字符字面值的表示

    2024年02月13日
    浏览(29)
  • Go简单实现协程池

    首先就是进程、线程、协程讲解老三样。 进程:  本质上是一个独立执行的程序,进程是操作系统进行资源分配和调度的基本概念,操作系统进行资源分配和调度的一个独立单位。 线程:  是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单

    2024年02月08日
    浏览(28)
  • go 协程并发数控制

    错误的写法: 这里的-ch 是为了从channel 中读取 数据,为了不使channel通道被写满,阻塞 go 协程数的创建。但是请注意,go workForDraw(v, wg) 是不阻塞后续的-ch 执行的,所以就一直go workForDraw(v, wg) 拉起新的协程。这么是达不到控制协程并发数10 的目的 正确的写法: 直接将-ch 写入

    2024年02月12日
    浏览(24)
  • 比特币如何运作?区块链、网络、交易

    免责声明:观点来自原文作者,与本人无关,文章仅供参考学习,请自行辨别真伪,切勿跟风,风险自担。 翻译原文:https://www.blockpit.io/blog/how-does-bitcoin-work 比特币是一种革命性的数字货币,由化名中本聪的匿名人士或团体于 2008 年发明。它是世界上第一个加密货币,负责将

    2024年03月24日
    浏览(38)
  • 为什么需要协调能力?如何提高协调能力?

    协调能力指的是协作与调和,属于综合性能力的体现,涉及到表达,沟通,逻辑等方面,在日常生活中缺乏协调能力,也许影响并不太大,但是如果在职业发展中,协调能力就尤为重要,尤其是某些职业岗位,对协调能力有更高的要求。 在我们的职业生涯中,团队协作是永恒

    2024年02月07日
    浏览(35)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包