七叶笔记 » golang编程 » golang协程调度原理

golang协程调度原理

什么是协程?

协程,又称微线程, 纤程 。英文名Coroutine。对于进程、 线程 ,都是有内核进行调度,有 CPU 时间片的概念,进行抢占式调度。协程的调用有点类似子程序,但是和子程序相比,协程有挂起的概念,协程可以挂起跳转执行其他协程,合适的时机再跳转回来。

协程的底层实现

线程是操作系统的内核对象, 多线程 编程时,如果线程数过多,就会导致频繁的上下文切换,这些 cpu 时间是一个额外的耗费。所以在一些高并发的 网络服务器 编程中,使用一个线程服务一个 socket 连接是很不明智的。于是操作系统提供了基于事件模式的异步编程模型。用少量的线程来服务大量的网络连接和I/O操作。但是采用异步和基于事件的编程模型,复杂化了程序代码的编写,非常容易出错。因为线程穿插,也提高排查错误的难度。

  协程,是在应用层模拟的线程,他避免了上下文切换的额外耗费,兼顾了多线程的优点。简化了高并发程序的复杂度。

协程对比线程的优势

1. 内存消耗方面 每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少。
     Go routine: 2KB
    线程:8MB

  2. 线程和 goroutine 切换调度开销方面 线程/ go routine 切换开销方面,goroutine 远比线程小
    线程: 涉及模式切换(从用户态切换到内核态)、16个 寄存器 PC 、SP…等寄存器的刷新等。
    goroutine: 只有三个寄存器的值修改 – PC / SP / DX.

golang协程调度原理

G(Goroutine):一个G代表一个goroutine

M(Machine):内核级线程,一个M代表了一个内核线程,等同于系统线程

P(Process):处理器,用来管理和执行goroutine,一个P代表了M所需的上下文环境

Sched:代表调度器,它维护有存储M和G的队列以及调度器的一些状态信息等。

G-M-P三者的关系与特点:

P的个数取决于设置的GOMAXPROCS,go新版本默认使用最大内核数,比如你有4核处理器,那么P的数量就是4,M的数量和P不一定匹配,可以设置很多M,M和P绑定后才可运行,多余的M处于休眠状态。P包含一个LRQ(Local Run Queue)本地运行队列,这里面保存着P需要执行的协程G的队列,除了每个P自身保存的G的队列外,调度器还拥有一个全局的G队列GRQ(Global Run Queue),这个队列存储的是所有未分配的协程G。
三者关系:G需要绑定在M上才能运行,M需要绑定P才能运行。
简单的来说,一个G的执行需要M和P的支持,一个M在与一个P关联之后形成了一个有效的G运行环境【内核线程 + 上下文环境】,每个P都会包含一个可运行的G的队列。

调度器的两大思想

复用线程:协程本身就是运行在一组线程之上,不需要频繁地创建、销毁线程,而是对线程的复用。在调度器中复用线程还有2个体现:1)work stealing,当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程。2)hand off,当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。

利用并行:GOMAXPROCS设置P的数量,当GOMAXPROCS大于1时,就最多有GOMAXPROCS个线程处于运行状态,这些线程可能分布在多个CPU核上同时运行,使得并发利用并行。另外,GOMAXPROCS也限制了并发的程度,比如GOMAXPROCS = 核数/2,则最多利用了一半的CPU核进行并行。

调度器的两个策略

抢占:在coroutine中要等待一个协程主动让出CPU才执行下一个协程,在Go中,一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死,这就是goroutine不同于coroutine的一个地方。全局G队列:在新的调度器中依然有全局G队列,但功能已经被弱化了,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G。

全局G队列:在新的调度器中依然有全局G队列,但功能已经被弱化了,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G。

golang协程通讯

Don’t communicate by sharing memory, share memory by communicating.

不要通过共享内存进行通信; 通过通信来共享内存

很多熟悉go语言的同学都应该熟悉的经典语句,在工程上,有两种最常见的并发通信模型: 共享内存 消息

golang 使用消息机制(channel)来作为通信模型,消息机制认为每个并发单元是自包含的、独立的个体,并且都有自己的变量,但在不同并发单元间这些变量不共享。每个并发单元的输入和输出只有一种,那就是消息。在 UNIX 中,select()函数用来监控一组描述符,该机制常被用于实现高并发的socket服务器程序。Go语言直接在语言级别支持select关键字,用于处理异步IO问题,大致结构如下:

 for {
  select {
      case <- chan:
      // 如果chan成功读到数据
    case <- chan1:
      // 如果chan1成功读到数据
      default:
      // 默认什么也不做
  }
}
  

channel使用技巧

1.向 close 的 channel 写数据、再次 close 都会触发 runtime panic。

2.向 nil channel 写、读取数据,都会阻塞,可以利用这点来进行优化 for + select 的用法。

3..channel 的关闭最好在写入方处理,读的协程不要去关闭 channel,可以通过单向通道来表明 channel 在该位置的功能。

4.如果有多个写协程的 channel 需要关闭,可以使用额外的 channel 来标记,也可以使用 sync.Once 或者 sync.Mutex 来处理。

5.channel 不管是读写都是并发安全的,不会出现多个协程同时读或者写的情况,从而实现了 CSP

相关文章