go-GMP 协程切换时机 和 协程实战

这篇具有很好参考价值的文章主要介绍了go-GMP 协程切换时机 和 协程实战。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

当m在执行某个g的时候,g非常耗时,例如一个for循环,每次循环sleep1分钟,循环1000次。

这个例子看似无聊,却是很难解决的,成功的避开了2个系统切换时机。

如果这个时候,一直执行这个g,别的g就会得不到执行,例如有g是处理用户支付的,这样就会造成收钱不积极。

协程饥饿问题

本地队列

go-GMP 协程切换时机 和 协程实战

本地队列因为 某个G一直 占着M,导致其他G无法执行。

如果占用时间过长的这个G,能让出来M,让别的G也能执行,本地队列循环的着执行,就能解决这个问题。

全局队列

go-GMP 协程切换时机 和 协程实战

除了本地队列,全局队列也会有这个问题,如果一个新创建的g,放在全局队列中,而现有的p的本地队列都未执行完,则全局队列需要排队很久。

解决办法,每过一段时间,每个本地队列都先来全局队列中取1个,这样就能解决这个问题。

代码实现:

又到了findRunnable()

// Check the global runnable queue once in a while to ensure fairness.
if pp.schedtick%61 == 0 && sched.runqsize > 0 {
	lock(&sched.lock)
	gp := globrunqget(pp, 1)
	unlock(&sched.lock)
	if gp != nil {
		return gp, false, false
	}
}

这个优先级在 本地队列之前。之前看过 globrunqget()中的逻辑,当max为1时候,就只会取一个。

每61次,就去全局队列中拿一个。

解决办法

协程因为独特的数据结构,能能够暂停的,之前协程的本质有介绍过,暂停后,让别的g也开始循环执行。

go-GMP 协程切换时机 和 协程实战

切换时机

主动挂起

go-GMP 协程切换时机 和 协程实战

业务方法主动调用gopark 然后,切换协程。

  源码在proc.go中
// Puts the current goroutine into a waiting state and calls unlockf on the
// 让当前的 g 进入 waiting的状态
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceReason traceBlockReason, traceskip int) {
	if reason != waitReasonSleep {
		checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
	}
	mp := acquirem()
	gp := mp.curg
	status := readgstatus(gp)
	if status != _Grunning && status != _Gscanrunning {
		throw("gopark: bad g status")
	}
	mp.waitlock = lock
	mp.waitunlockf = unlockf
	gp.waitreason = reason
	mp.waitTraceBlockReason = traceReason
	mp.waitTraceSkip = traceskip
	releasem(mp)
	// can't do anything that might move the G between Ms here.
	mcall(park_m) // 切换到了 g0栈,前面讲过mcall
}

// park continuation on g0.
func park_m(gp *g) {
	mp := getg().m
	// 中间还有代码
	if fn := mp.waitunlockf; fn != nil {
		ok := fn(gp, mp.waitlock)
		mp.waitunlockf = nil
		mp.waitlock = nil
		if !ok {
			if traceEnabled() {
				traceGoUnpark(gp, 2)
			}
			casgstatus(gp, _Gwaiting, _Grunnable)
			execute(gp, true) // Schedule it back, never returns.
		}
	}
      schedule() //调了这个方法,之前讲过,一旦调用这个方法,就会给m找新的g
}

有个问题:gapark是小写的,程序员在编码中是使用不了的,那怎么让业务主动调用?

系统runtime里面很多方法有去调用,例如 time.Sleep、channel的等待等

系统调用完成时

go-GMP 协程切换时机 和 协程实战

在进行一些系统调用后,例如网络请求等会主动去调这个 exitsyscall() 这个方法,这个方法也会最终走到 schedule()

这个源码在 syscall_aix.go中,因为有好几层函数调用,就不贴出来了。

标记抢占 基于 morestack

 1. 系统监控到 Goroutine 运行超过 10ms
 2. 将 g.stackguard0 置为 Oxfffffade  
go-GMP 协程切换时机 和 协程实战

morestack() 方法,在函数跳转时候,会自动调用,本意是检测下新的函数,有没有足够的栈空间。

系统在这个函数中,去做了一部分协程切换的,防止一些 耗时比较久的协程,不去触发上面两种方案。

看下源码:

// Called during function prolog when more stack is needed.
// record an argument size. For that purpose, it has no arguments.
TEXT runtime·morestack(SB),NOSPLIT,$0-0

  // 中间很多扩充栈空间的代码
    CALL	runtime·newstack(SB) // 跟进这个方法
	CALL	runtime·abort(SB)	// crash if newstack returns
	RET


// Goroutine preemption request.
// 0xfffffade in hex.
stackPreempt = uintptrMask & -1314

func newstack() {
 // 中间还有很多源码
  // 抢占标记   如果g的.stackguard0 字段被标记为抢占,就会触发下面的逻辑
  stackguard0 := atomic.Loaduintptr(&gp.stackguard0)
  preempt := stackguard0 == stackPreempt 
  if preempt {
        // Act like goroutine called runtime.Gosched.
        gopreempt_m(gp) // never return
      }
}

 func gopreempt_m(gp *g) {
  	if traceEnabled() {
  		traceGoPreempt()
  	}
  	goschedImpl(gp)
  }
  
 func goschedImpl(gp *g) {
	//删了一些源码,最终调了 schedule 
	schedule()
}

基于信号的抢占标记

开头那个 for 循环的例子,虽然很无聊,但是去避开了上面那种方案,
1. 不会调用 gopark
2. 不会系统调用
3.不会调用 morestack,因为没有函数调用

这时候,可以使用 信号 来触发协程的切换。

信号量可以在多线程和多进程直接进行通信(管道、共享内存、信号、消息队列一般作为多进程通信方式)。

原理:

操作系统中,有很多基于信号的底层通信方式,例如: SIGPIPE / SIGURG / SIGHUP
线程可以注册对应信号的处理函数

go的实现流程:

注册 `SIGURG`信号的处理函数

`GC`工作时,向目标线程发送信号

线程收到信号,触发调度,`gc`发送 `sigurg` 触发`runtime`的 `doSigPreempt()`
go-GMP 协程切换时机 和 协程实战

能够猜到 doSigPreempt(), 最终会去调 schedule() 方法。 源码就不贴了。

开发中,协程过多的问题

  1. 文件打开数限制
      过多协程调用文件读写,会操作系统崩溃。

  2. 内存限制
      过多协程创建,达到了内存的限制  
  
  3. 调度开销过大
      过多协程,导致调度器调度复杂度增大

解决办法:

    1. 优化业务逻辑

    2. 利用 channel 的缓存区

    3. 协程池

    4. 调整系统资源

2和3都是从控制协程的数量入手,2适合 单个业务场景,3适合全局。

1和4好理解

利用 channel 的缓存区

func do(c chan interface{}) {

	fmt.Println("do it")
	<-c
}

func main() {
    
  // 利用channel的特性来控制 go协程的个数
	ch := make(chan interface{}, 100)
	for {
		ch <- struct{}{}
		go do(ch)
	}
}

这种适合在一个场景下适用,不推荐全局使用。

协程池

代表:https://github.com/Jeffail/tunny

原理:类似某些语言的线程池

    1.预创建一定数量的协程
    2.将任务送入协程池队列
    3.协程池不断取出可用协程,执行任务
go-GMP 协程切换时机 和 协程实战

慎用协程池

Go语言的线程,已经相当于池化了

二级池化会增加系统复杂度

Go语言的初衷是希望协程即用即毁,不要池化

到此,go的GMP完结。文章来源地址https://www.toymoban.com/news/detail-747831.html

到了这里,关于go-GMP 协程切换时机 和 协程实战的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • 在使用TensorFlow的时候内部报错:内部某个方法或属性不存在

    看到TensorFlow内部封装的方法报错的时候,我的第一反应是版本不匹配,立马去搜了对应版本,按照网上给的TensorFlow 2.2.0+keras 2.3.1 +python 3.7,反反复复安装、卸载、升级、降低版本了很多回还是八行,就在心态快要爆爆爆炸的时候,试了下面这条命令: 然后再运行程序,成功

    2024年02月14日
    浏览(34)
  • git操作--->在远程删除了某个分支,但本地使用git branch -r的时候还是会显示某个分支存在是什么原因

    💕又迷糊了哈哈,以为自己命令执行错了,结果可能是缓存的原因:💕 😂如果你发现使用 git branch -r 命令显示了一个远程没有的分支,这可能是由以下几个原因造成的:😂 缓存的远程分支信息: 当你克隆一个仓库或者与远程仓库交互时,Git 会在本地保存远程分支的缓存信

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

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

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

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

    2024年02月12日
    浏览(34)
  • Go语言入门12(协程 goroutine)

    进程 ​当运行一个应用程序的时候,操作系统会为这个应用程序启动一个进程。可以将这个进程看作一个包含了应用程序在运行中需要用到和维护的各种资源的容器。这些资源包括但不限于内存地址空间、文件和设备的句柄以及线程 线程 ​一个线程是一个执行空间,这个空

    2023年04月26日
    浏览(39)
  • go协程、线程的本质,如何协调运作

    线程在创建、切换、销毁时候,需要消耗CPU的资源。 协程就是将一段程序的运行状态打包, 可以在线程之间调度。减少CPU在操作线程的消耗 协程、线程、进程 这块网上非常多文章讲了,就不多叙述了。 归纳下: 上面讲了 ,协程的本质就是 一段程序的运行状态的打包: 例

    2024年02月05日
    浏览(54)
  • go的协程和管道运用案例

    2024年01月19日
    浏览(37)
  • Flutter实现CombineExecutor进行多个异步分组监听,监听第一个异步执行的开始和最后一个异步执行结束时机。

    1.场景 我们在调用接口时,很多时候会同时调用多个接口,接口都是异步执行,我们很难知道调用的多个接口哪个会最后执行完成,我们有时候需要对最后一个接口执行完成的时机监听,所以基于该需求,设计了CombineExecutor,对类似的需求进行监听。 2.代码 group_key.dart execu

    2024年02月09日
    浏览(32)
  • 关于 Go 协同程序(Coroutines 协程)、Go 汇编及一些注意事项。

    参考:  Go 汇编函数 - Go 语言高级编程 Go 嵌套汇编 - 掘金 (juejin.cn) 前言: Golang 适用 Go-Runtime(Go 运行时,嵌入在被编译的PE可执行文件之中)来管理调度协同程式的运行。 Go 语言没有多线程(MT)的概念,在 Go 语言之中,每个 Go 协程就类似开辟了一个新的线程,效率上,肯

    2024年01月25日
    浏览(69)
  • 协程切换原理与实践 -- 从ucontext api到x86_64汇编

    目录 1.协程切换原理理解 2.ucontext实现协程切换 2.1 实现流程 2.2 根据ucontext流程看协程实现 2.3 回答开头提出的问题 3.x86_64汇编实现协程切换 3.1libco x86_64汇编代码分析 3.2.保存程序返回代码地址流程 3.3.恢复程序地址以及上下文  4.实现简单协程框架 协程可以实现在一个线程中

    2024年02月05日
    浏览(41)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包