唤醒手腕 Go 语言 并发编程 (goroutine、channel)详细教程(更新中)

线程、协程基本概念

协程是单线程下的并发,又称微线程,纤程。它是实现多任务的另一种方式,只不过是比线程更小的执行单元。因为它自带CPU的上下文,这样只要在合适的时机,我们可以把一个协程切换到另一个协程。英文名Coroutine。

一句话说明什么是协程?

轻量级的线程独立的栈空间,共享程序堆空间调度由用户控制是逻辑态,对资源消耗小。

线程和协程的区别

线程的切换是一个Cpu在不同线程中来回切换,是从系统层面来,不止保存和恢复CPU上下文这么简单,会非常耗费性能。但是协程只是在同一个线程内来回切换不同的函数,只是简单的操作CPU的上下文,所以耗费的性能会大大减少。

golang的协程机制,可轻松开启上万个协程。其他语言并发机制一般基于线程,开启过多资源耗费大。

func runTimes(nums int) {
    
    
	for i := 0; i < nums; i++ {
    
    
		fmt.Println("runTimes", i, "times")
		time.Sleep(1000 * time.Millisecond)
	}
}

func main() {
    
    
	go runTimes(10)
	for i := 0; i < 10; i++ {
    
    
		fmt.Println("main", i, "times")
		time.Sleep(2000 * time.Millisecond)
	}
}

注意点:如果协程没有执行完,但是主线程已经结束,协程会直接结束。协程在主线程之前结束。那么协程的任务就完成了。

Go 语言 goroutine

在java/c++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务,同时需要自己去调度线程执行任务并维护上下文切换,这一切通常会耗费程序员大量的心智。

为此Go语言提供了 goroutine 这样的机制,goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。

使用 goroutine

有时 testRun() 并不执行,因为程序会为main函数创建一个默认的goroutine,当main里的语句执行完goroutine也就结束了,没有 go testRun() 执行的时间。为了确保go hello() 的goroutine能够执行可以延缓程序结束时间

func main() {
    
    
	go testRun()
	fmt.Println("main hello")
	time.Sleep(time.Second)
}

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

单个 goroutine 可以通过时间延后来使这个 goroutine 被完全执行,但是当 goroutine 多到上百上千或更多时在使用 time.Sleep() 显然就没办法确定给多少时间来让 goroutine 被完全执行了,给多了影响程序效率,给少了有的 goroutine 又不会执行影响程序结果,这时候我们就要用到另一个东西那就是 sync.WaitGroup。

WaitGroup 对象内部有个计时器, 最初从0 开始, 他有3个方法 Add() , Done(), Wait() 用来控制计数器的数量。 Add(n) 把计数器设置成 n,Done() 每次把计数器 -1, wait() 会阻塞代码的运行, 直到计数器的值减为 0。将 goroutine 所剩数量与 WaitGroup 结合可以解决上述问题。

var wg sync.WaitGroup

func main() {
    
    
	for i := 0; i < 10; i++ {
    
    
		wg.Add(1)
		go printNum(i)
	}
	wg.Wait()
	fmt.Println("end")
}

func printNum(i int) {
    
    
	defer wg.Done()
	fmt.Println(i)
}

当我们运行这个代码就会发现每次的输出都不同,这是因为这 10 个 goroutine 的执行是并发的,而调度却是随机的。

goroutine 栈内存

OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这个大。所以在Go语言中一次创建十万左右的 goroutine 也是可以的。

goroutine 调度

GPM 是Go语言运行时(runtime)层面的实现,是 go 语言自己实现的一套调度系统。区别于操作系统调度OS线程。

  1. G 就是个goroutine的,里面除了存放本 goroutine 信息外,还有与所在 P 的绑定等信息。
  2. P 管理着一组 goroutine 队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的 goroutine 队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
  3. M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一对一映射的关系, 一个groutine最终是要放到 M 上执行的。
在这里插入代码片

Go 语言 runtime 库

设置CPU最大核心数量

runtime.GOMAXPROCS(8)

查看当前CPU当前核心数量

fmt.Println(runtime.NumCPU())

runtime.Gosched()

退出当前的 goroutine ,为其他 goroutine 腾出执行空间,最后再执行被退出的 goroutine

大概意思就是本来计划的好好的周末出去烧烤,但是你妈让你去相亲,两种情况第一就是你相亲速度非常快,见面就黄不耽误你继续烧烤,第二种情况就是你相亲速度特别慢,见面就是你侬我侬的,耽误了烧烤,但是还馋就是耽误了烧烤你还得去烧烤。

func main() {
    
    
	// 让所有协程在一个核上执行
	runtime.GOMAXPROCS(1)
	go func(s string) {
    
    
		for i := 0; i < 2; i++ {
    
    
			fmt.Println(s,i)
		}
	}("协程运行中")
	// 主协程
	for i := 0; i < 2; i++ {
    
    
		fmt.Println("hello")
		// 停一下,再次分配任务
		runtime.Gosched()
		fmt.Println("world",i)
	}
}

runtime.Goexit()

退出当前的 goroutine ,以后也不会执行。

一边烧烤一边相亲,突然发现相亲对象太丑影响烧烤,果断让她滚蛋,然后也就没有然后了

runtime.GOMAXPROCS

Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。

Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。

操作系统线程 vs goroutine关系

1. 一个操作系统线程对应用户态多个goroutine。
2. go程序可以同时使用多个操作系统线程。
3. goroutine和OS线程是多对多的关系,即m:n。

Go 语言 channel 管道

我们设置函数的意义就是为了在特定的输入下获取到特定的输出,如果只是让函数一味的并发而不进行值的传递,那么这个并发就是没有意义的。

如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

channel是个特殊的类型,通道类似传送带或是队列,遵循先进先出(First In First Out)的原则。

通道的声明和初始化

每个通道都是特定类型的,在声明时需要指明通道里传输的元素类型。

var 变量 chan 元素类型
var ch chan int
println(ch)	

声明的通道后需要使用make函数初始化之后才能使用。初始化后的通道空值为一个十六进制的地址。

`在不进行初始化的情况下使用通道会报 deadlock`
`其中缓存大小是可选项`
make(chan 元素类型, [缓冲大小])

var ch1,ch2 chan int
ch1 = make(chan int)	`无缓存的通道ch1`
ch2 = make(chan int,20)	`缓存大小为20的通道ch2`

直接定义通道

ch3 := make(chan int)

猜你喜欢

转载自blog.csdn.net/qq_47452807/article/details/128673618