go语言基础-----16-----goroutine、GPM模型

1 如何使用Goroutine

在函数或方法调用前面加上关键字go,您将会同时运行一个新的Goroutine。例如:

// hi为一个函数
go hi()

2 子协程异常退出的影响

在使用子协程时一定要特别注意保护好每个子协程,确保它们正常安全的运行。因为子协程的异常退出会将异常传播到主协程,直接会导致主协程也跟着挂掉,然后整个程序就崩溃了。
例如:

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("run in main goroutine")

	go func() {
		go func() {
			defer fmt.Println("异常了")

			fmt.Println("run in grand child goroutine")

			var ptr *int
			*ptr = 0x12345 // 故意制造崩溃
			fmt.Println("xx")
			
			go func() {
				fmt.Println("run in grand grand child goroutine")
			}()
		}()

		time.Sleep(time.Second * 1) // 确保上面的子协程执行完毕,否则程序是并行的,可能会先打印222 end,会影响测试。
		fmt.Println("222 end")
	}()

	time.Sleep(time.Second * 3)
	fmt.Println("main goroutine will quit")
}

结果看到,在含有defer语句的协程崩溃,导致了其父协程、子协程都终止,最终程序崩溃掉。
在这里插入图片描述

3 协程异常处理-recover

在前面说过了recover是可以捕捉异常的,所以我们可以使用它进行协程的异常处理。

recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover 仅在延迟函数 defer 中有效。如果当前的 goroutine 陷入panic,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。

// 1.3 协程异常处理-recover

package main

import (
	"fmt"
	"runtime"
)

// 崩溃时需要传递的上下文信息
type panicContext struct {
	function string // 所在函数
}

// 保护方式允许一个函数
func ProtectRun(entry func()) {
	// 延迟处理的函数
	defer func() {
		// 发生宕机时,获取panic传递的上下文并打印
		err := recover()
		if err != nil {
			switch err.(type) {
			case runtime.Error: // 运行时错误
				fmt.Println("runtime error:", err)
			default: // 非运行时错误
				fmt.Println("error:", err)
			}
		} else {
			fmt.Println("no error")
		}

	}()

	// 执行函数
	entry()
}

func main() {
	fmt.Println("运行前")

	// 1. 允许一段手动触发的错误,人为制造的错误,代码实际没错。
	ProtectRun(func() {
		fmt.Println("手动宕机前")
		// 使用panic传递上下文
		//a := &panicContext{"手动触发panic"}
		//panic(a)	// 这样传,或者下面直接传都行
		panic(&panicContext{"手动触发panic"})
		fmt.Println("手动宕机后")
	})

	// 2. 故意造成空指针访问错误,代码真的有错。
	ProtectRun(func() {
		fmt.Println("赋值宕机前")
		var a *int
		*a = 1
		fmt.Println("赋值宕机后")
	})

	fmt.Println("运行后") // 这里看到打印,说明使用了recover函数后,程序出了问题,但是并不会崩溃。
}

结果看到。即使出现代码问题,程序也不会崩溃,而是照常执行。
在这里插入图片描述

4 启动百万协程

Go 语言能同时管理上百万的协程,一般启动百万协程,内存大概占用4G-4.8G左右,对应一些内存大的服务器,例如128G,是完全没问题的。

// 1-4 启动百万协程

package main

import (
	"fmt"
	"runtime"
	"time"
)

const N = 1000000

func main() {
	fmt.Println("run in main goroutine")
	i := 1
	for {
		go func() {
			// 每个开启的协程都死循环,并睡1s防止占用CPU造成电脑卡死。
			for {
				time.Sleep(time.Second)
			}
		}()
		if i%10000 == 0 {
			fmt.Printf("%d goroutine started\n", i)
		}

		i++
		if i == N {
			break
		}
	}

	fmt.Println("NumGoroutine:", runtime.NumGoroutine())
	time.Sleep(time.Second * 15)
}

在vscode测试,执行前内存占用0.4G,执行完后占用4.8G,即go的百万协程占用4.4G左右。
在这里插入图片描述
在这里插入图片描述

5 死循环

如果有个别协程死循环了会导致其它协程饥饿得到不运行吗?
答:并不会。

测试代码:

package main

import (
	"fmt"
	"runtime"
	"syscall"
	"time"
)

// 获取的是线程ID,不是协程ID
func GetCurrentThreadId() int {
	var user32 *syscall.DLL
	var GetCurrentThreadId *syscall.Proc
	var err error

	user32, err = syscall.LoadDLL("Kernel32.dll") // Windows用的
	if err != nil {
		fmt.Printf("syscall.LoadDLL fail: %v\n", err.Error())
		return 0
	}
	GetCurrentThreadId, err = user32.FindProc("GetCurrentThreadId")
	if err != nil {
		fmt.Printf("user32.FindProc fail: %v\n", err.Error())
		return 0
	}

	var pid uintptr
	pid, _, err = GetCurrentThreadId.Call()

	return int(pid)
}

func main() {

	//runtime.GOMAXPROCS:该函数的作用是设置当前进程使用的最大cpu数,返回值为上一次调用成功的设置值,首次调用返回的是默认值(例如cpu为4核则返回值为4)。
	// runtime.GOMAXPROCS(1)	// 设置CPU并发数为1核。
	fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) // 传0代表使用默认值
	fmt.Println("run in main goroutine")
	n := 5
	for i := 0; i < n; i++ {
		go func() {
			fmt.Println("dead loop goroutine start, threadId:", GetCurrentThreadId())
			for {
			} // 死循环
			fmt.Println("dead loop goroutine stop")
		}()
	}

	go func() {
		var count = 0
		for {
			time.Sleep(time.Second)
			count++
			fmt.Println("for goroutine running:", count, "threadId:", GetCurrentThreadId())
		}
	}()

	fmt.Println("NumGoroutine: ", runtime.NumGoroutine()) // 打印当前程序正在运行的协程数量,这里是7.因为上面for开了5个,再单独go一个,最后加上main这个主协程就是7个。

	var count = 0
	for {
		time.Sleep(time.Second)
		count++
		fmt.Println("main goroutine running:", count, "threadId:", GetCurrentThreadId())
	}
}

取出部分打印来看,即使有部分协程在进行死循环操作,但是其他协程仍然可以执行。

看到死循环的线程ID可能一样,这是因为同一个线程可以有多个协程。

在这里插入图片描述

6 设置线程数

Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码。其默认的值是运行机器上的CPU的核心数,所以在一个有8个核心的机器上时,调度器一次会在8个OS线程上去调度GO代码。

package main

import (
	"fmt"
	"runtime"
)

func main() {
	fmt.Println("runtime.NumCPU():", runtime.NumCPU()) // 获取CPU的最大核数

	// 读取默认的线程数
	fmt.Println(runtime.GOMAXPROCS(0))

	// 设置线程数为 10
	runtime.GOMAXPROCS(10)
	// 读取当前的线程数
	fmt.Println(runtime.GOMAXPROCS(0))

	// 设置最大cpu数
	runtime.GOMAXPROCS(runtime.NumCPU())
	fmt.Println(runtime.GOMAXPROCS(0))
}

例如这是我的电脑配置,注意不同人可能会因不同电脑配置,导致打印结果不一样。
在这里插入图片描述

7 G-P-M模型

7.1 为什么引入协程?
核心原因为goroutine的轻量级,无论是从进程到线程,还是从线程到协程,其核心都是为了使得我们的调度单元更加轻量级。可以轻易得创建几万几十万的goroutine而不用担心内存耗尽等问题。
在这里插入图片描述
其中:

  • M Machine :os线程(即操作系统内核提供的线程)。
  • G:goroutine,其包含了调度一个协程所需要的堆栈以及instruction pointer(IP指令指针) ,以及其他一些重要的调度信息。
  • P Process :M与P的中介,实现m:n 调度模型的关键,M必须拿到P才能对G进行调度,P其实限定了golang调度其的最大并发度。表示一个逻辑处理器一个p绑定一个os线程。

7.2 系统调用
在这里插入图片描述

  • 调用system call(系统调用)进入内核没有返回之前,为保证调度的并发性,golang 调度器在进入系统调用之前从线程池拿一个线程或者新建一个线程,当前P交给新的线程M1执行。
  • G0返回之后,需要找一个可用的P继续运行,如果没有则将其放在全局队列等待调度。M0待G0返回后退出或放回线程池。

7.3 工作流窃取

  • 在P队列上的goroutine全部调度完了之后,对应的M首先会尝试从global runqueue中获取goroutine进行调度。如果golbal runqueue中没有goroutine,当前M会从别的M对应P的local runqueue中抢一半的goroutine放入自己的P中进行调度。
    简单说就是,当自己的线程的P调度器没有goroutine时,会从其它线程的P调度器中获取goroutine,这就是工作流窃取。
    具体要看C代码去了。
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值