Go轻量级线程Goroutine

文章目录

    • Goroutine
        • goroutine的创建
        • GPM介绍
        • 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介绍

Go轻量级线程Goroutine_第1张图片

操作系统的线程一般都有固定的栈(通常为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并不是按顺序执行的,所以最终打印的结果就是:所有数字都会打印出来,只是顺序不同

你可能感兴趣的:(Golang精进之路,golang,开发语言,后端)