带你了解什么是Goroutine

道阻且长,行则将至,行而不辍,未来可期🌟。人生是一条且漫长且充满荆棘的道路,一路上充斥着各种欲望与诱惑,不断学习,不断修炼,不悔昨日,不畏将来!
希望这篇能够带给你们或多或少的帮助

Goroutine

  • Golang中最迷人的一个优点就是从语言层面就支持并发
  • 在Golang中的goroutine(协程)类似于其他语言的线程

我们可以理解goroutine是一个轻量级线程,所占用的栈空间很小(一般为2KB)且可以扩容与减小

goroutine的创建

在Go语言内可以通过go func来创建一个goroutine

package main

func hello()  {
	fmt.Println("Hello")
}

func main() {
	go hello()

	fmt.Println("Main")
}

Go程序开始时会创建一个 main goroutine。我们通过go关键字也会创建一个goroutine去执行hello程序,而此时main goroutine会继续执行。
此时程序里面会有两个goroutine并发执行,当main函数结束后 main goroutine会结束,由main goroutine创建的其它goroutine也会立即结束
所以这里hello是有可能打印不出来的

执行结果:

Main

所以为了避免main函数代码运行结束,而其它goroutine仍在运行中,所以我们需要main函数阻塞等待其它goroutine执行结束

暴力等待可通过time.Sleep 。这里使用sync模块,利用计数等待实现

package main

import (
	"fmt"
	"sync"
)

var ws sync.WaitGroup

func hello() {
	// goroutine函数结束后,Done表示计数减1
	defer ws.Done()

	fmt.Println("Hello")
}

func main() {
	// 开启一个goroutine之前进行计数
	ws.Add(1)

	go hello()

	fmt.Println("Main")

	// 当计数为0的时候才结束,否则阻塞等待
	ws.Wait()
}

执行结果:

Main
Hello

GPM介绍

在这里插入图片描述

操作系统的线程一般都有固定的栈(通常为2MB),而Go语言中的goroutine非常轻量级,一个goroutine的初始栈空间很小(一般为2KB),并且goroutine的栈空间大小不是固定的,通常可以根据内容进行扩容增大或减小,Go的runtime会自动分配合适的goroutine的栈空间。

由于线程间切换需要进行一个完整的上下文切换过程开销较大,Go语言本身具有一套调度goroutine的系统。它将按照一定规则将goroutine调度到操作系统线程上执行,经过数个版本的迭代,Go语言调度器目前按照GPM模型

  • G:表示goroutine,通过go func关键字创建,包含执行的函数和上下文信息

    全局队列:存放等待运行的G

  • P:表示goroutine执行所需要的资源,最多有GOMAXPROCS个(CPU的核心)

    P本地队列:也是存放等待运行G的地方,创建G之后会优先放到P的队列,不过P队列存放的G数量有限,不超过256。如果P本地队列满了则会批量移动部分G到全局队列

  • M:线程想运行任务就得获取P。从P本地队列获取G,如果P队列为空,则尝试从其它的P队列获取都没有的话则尝试从全局队列获取。M运行G,G执行之后,M又会继续获取G,不断重复

goroutine调度器和操作调度器是通过M结合的,每个M代表1个内核线程,操作系统调度器负责把内核线程分配到CPU核上运行

从线程来讲:Go语言的goroutine相较于其它语言开启线程的优势在于,其它线程的线程是由内核来调度的,goroutine则是Go语言运行时自己调度的,完全是在用户态下面完成的,不涉及内核与用户态之间频繁切换

goroutine调度

goroutine的执行顺序并不是按照创建时间来的,而是取决于哪个G优先被M执行


猜测该函数执行后的打印结果:

func f() {
	ws.Add(5)
	for i := 0; i < 5; i++ {
		go func() {
			fmt.Println(i)
			ws.Done()
		}()
	}
}

执行结果:

5
5
5
5
5

程序解析:

go关键字会创建goroutine去执行里面的匿名函数,而从创建->执行中间需要一定的时间,而for循环却在继续,匿名函数里面访问的是外部变量i,所以在实际执行到这个goroutine的时候,循环可能已经结束了,而i的值最终会变成5。所以大部分情况会打印5 5 5 5 5。也可能存在goroutine执行较快的情况,某个goroutine在循环未结束之前就执行了,可能会打印出其它数字。

猜测该函数执行后的打印结果:

func f2() {
	ws.Add(5)
	for i := 0; i < 5; i++ {
		go func(j int) {
			fmt.Println(j)
			ws.Done()
		}(i)
}

程序解析:

该函数与f不同的是,i由于传递到了匿名函数里面,那么打印的值是确认的,而由于goroutine并不是按顺序执行的,所以最终打印的结果就是:所有数字都会打印出来,只是顺序不同

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值