Golang协程和通道

协程(goroutine)

基本介绍

基本介绍

进程、线程与协程:

  • 进程(Process)是计算机中正在运行的程序的实例,是操作系统进行资源分配和调度的基本单位。每个进程都有自己独立的地址空间、代码、数据和文件资源。进程之间相互独立,通过进程间通信机制进行数据交换和协作。进程的创建、销毁以及切换都由操作系统自动完成,开销较大。
  • 线程(Thread)是操作系统调度的最小执行单元,是进程内的一个执行路径。线程与进程共享同一地址空间和大部分资源,包括代码段、数据段和打开的文件等。线程之间通常借助互斥锁、条件变量以及信号量等进行数据交换。线程的创建、销毁以及切换的开销较小,但需要注意线程间的同步和共享资源的管理。
  • 协程(Coroutine)协程是一种轻量级的并发执行单元,通常由编程语言本身的运行时系统进行调度和管理。协程通常在一个线程内执行,共享相同的地址空间和资源。协程间通常通过通道(Channel)实现数据交换和协作。协程的创建、销毁以及切换都由运行时系统自动完成,开销非常小,可以创建成千上万个协程而不会导致系统负载过高。

并发与并行:

  • 并发(Concurrency)指的是在单个处理器上以时间片轮转的方式交替执行多个任务,使得在一段时间内,这多个任务都得以推进,但实际在一个时间点只有一个任务在执行。
  • 并行(Parallelism)指的是多个任务同时在不同的处理器上执行,使得这多个任务同时得以推进,并且在一个时间点来看,也是多个任务在同时执行。

在Go中,通过在函数或方法的调用前加上go关键字即可创建一个go协程,并让其运行对应的函数或方法。如下:

package main

import (
	"fmt"
	"time"
)

func Print() bool {
   
	for i := 0; i < 10; i++ {
   
		fmt.Printf("Print: hello goroutine...%d\n", i+1)
		time.Sleep(time.Second)
	}
	return true
}

func main() {
   
	go Print() // 创建go协程

	for i := 0; i < 5; i++ {
   
		fmt.Printf("main: hello goroutine...%d\n", i+1)
		time.Sleep(time.Second)
	}
}

在上述代码中,主协程创建了一个新协程用于执行Print函数,主协程进行5次打印后退出,新协程进行10次打印后退出。运行结果如下:

在这里插入图片描述

说明一下:

  • 在Go中,当程序启动时会自动创建一个主协程来执行main函数,该协程与其他新创建的协程没有本质的区别,但主协程执行完毕后整个程序会退出,即使其他协程还未执行完毕,也会跟着退出。
  • 如果一个协程在执行过程中触发了panic异常,但没有对其进行捕获,那么会导致整个程序崩溃,因此在协程中也需要通过recover函数对panic进行捕获。

GMP模型

常规的协程(Coroutine)

线程是在内核态视角下的最小执行单元,而协程是在线程的基础上,在用户态视角下进行二次开发得到的更小的执行单元。常规的协程(Coroutine)通常是与一个线程强绑定的,而一个线程可以绑定多个协程。如下:

在这里插入图片描述

说明一下:

  • 由于常规的协程是与一个线程强绑定的,因此绑定于同一线程的多个协程只能做到并发,无法做到并行。
  • 当一个协程因为某些原因陷入阻塞,那么这个阻塞会直接上升到对应的线程,最终导致整个协程组陷入阻塞。

Go中的协程(Goroutine)

Go语言中的协程(Goroutine)与常规的协程(Coroutine)的实现方式有所不同,Go中的协程不是与一个线程强绑定的,而是由Go调度器动态的将协程绑定到可用的线程上执行。如下:

在这里插入图片描述

说明一下:

  • 由于Go协程与线程之间的绑定是动态的,因此各个协程之间既能做到并发,也能做到并行。
  • 当一个Go协程因为某些原因陷入阻塞,那么Go调度器会将该协程与其绑定的线程进行解绑,将线程的资源释放出来,使得线程可以与其他可调度的协程进行绑定。

GMP模型

GMP(Goroutine-Machine-Processor)模型是Go运行时系统中用于实现并发执行的模型,负责管理和调度协程的执行。G、M和P的含义分别如下:

  • G(Goroutine):代表Go中的协程,每个G都有自己的运行栈、状态以及执行的任务函数。
  • M(Machine):代表Go中的线程,M不直接执行G,而是先和P绑定,由P来指定M所需执行的G。
  • P(Processor):代表Go中的调度器,P实现G和M之间的动态有机结合。对于G而言,P就是其CPU,G只有被P调度才得以执行;对于M而言,P是其执行代理,为其指定可执行的G。

GMP模型示意图如下:

在这里插入图片描述

上图说明:

  • 全局有多个M和多个P,但M和P的数量不一定是相同的。每个M在调度G之前,需要先和P进行绑定(不是强绑定),每个M调度的G由其对应的P指定。M无需记录所调度的G的状态信息,因此G在全生命周期中可以实现跨M执行。
  • 在GMP模型中有三种队列来存放G,分别是全局队列、P的本地队列和wait队列(用于存放io阻塞就绪态的G,图中未展示)。
  • 每个P都有一个对应本地队列,访问本地队列时可以接近无锁化。当P为M获取可调度的G时,会优先从自己的本地队列中进行获取,其次从全局队列中获取,最后从wait队列中获取。
  • 如果一个G在调度过程中新创建了一个G,那么这个新G会优先投递到当前P的本地队列中,如果本地队列已满则投递到全局队列中。

调度器P获取可调度的G的流程如下:

  1. 优先尝试从当前P的本地队列获取可调度的G。
  2. 尝试从全局队列获取可调度的G。
  3. 尝试从wait队列获取io阻塞就绪的G。
  4. 尝试从其他P的本地队列窃取一半的G补充到当前P的本地队列,防止不同P的闲忙差异过大(work-stealing机制)。

说明一下:

  • 由于存在work-stealing机制,因此P的本地队列的访问也不是完全无锁的,只能说接近无锁化。
  • 上述说到的只是获取可调度的G的主要流程,实际实现时还有更多的细节。比如P每进行61次调度后,会先尝试从全局队列中获取一个G进行调度,避免造成全局队列中的G的饥饿问题。

GOMAXPROCS

在GMP模型中,G只有被P调度才得以执行,因此P的数量决定了G的最大并行数量。通过runtime包中的GOMAXPROCS函数可以获取和设置P的数量。如下:

package main

import (
	"fmt"
	"runtime"
)

func main() {
   
	cpuNum := runtime.NumCPU()          // 获取本地机器的逻辑CPU数
	fmt.Printf("cpuNum = %d\n", cpuNum) // cpuNum = 6

	runtime.GOMAXPROCS(4)         // 设置可同时执行的最大CPU数
	num := runtime.GOMAXPROCS(0)  // 获取可同时执行的最大CPU数
	fmt.Printf("num = %d\n", num) // num = 4
}

说明一下:

  • runtime包中的NumCPU函数,用于获取本地机器的逻辑CPU数。
  • runtime包中的GOMAXPROCS函数,用于设置可同时执行的最大CPU数,并返回先前的设置。如果设置的值小于1,则不会更改当前的值,设置的值超过CPU核数无意义。
  • 从Go1.5开始,GOMAXPROCS默认设置为CPU的核数,并且可以根据需要自动调整并发执行的并行度,无需再手动设置。

协程的生命周期

Go中协程的生命周期大致由如下几种状态组成:

  • _Gidle:表示该协程刚刚创建,但还未进行初始化。
  • _Gdead:表示该协程已经完成初始化,但还未被使用。
  • _Grunnable:表示该协程已经被放入运行队列,但还未被调度。
  • _Grunning
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

2021dragon

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值