Golang 的调度策略

我们上篇文章(Golang 的底层引导流程/启动顺序)介绍了一个golang程序的启动流程,在文章的最后对于最重要的一点“调度“ (函数  schedule()) 并没有展开来讲,今天我们继续从源码来分析一下它的调度机制。

在此之前我们要明白golang中的调度主要指的是什么?这里指如何找一个G 关联到PM 让其执行。对于G 的调度可以围绕三个方面来理解:

  • 时机:什么时候关联(调度)。对于调度时机一般是指有空闲P的时候都会去找G执行
  • 对象:选择哪个G进行调度。这是我们本篇要讲的内容
  • 机制:如何调度。execute() 函数

理解了这三个问题,基本也就明白了它的调度策略了,本篇主要对G的获取。

源文件 src/runtime/proc.go , go version 1.15.6

Continue reading

Runtime: Golang是如何处理系统调用阻塞的?

我们知道在Golang中,当一个Goroutine由于执行 系统调用 而阻塞时,会将M从GPM中分离出去,然后P再找一个G和M重新执行,避免浪费CPU资源,那么在内部又是如何实现的呢?今天我们还是通过访问Runtime源码的形式来看下他的内部实现细节有哪些?

go version 1.15.6

我们知道一个P有四种运行状态,而当执行系统调用函数阻塞时,会从 _Prunning 状态切换到 _Psyscall,等系统调用函数执行完毕后再切换回来。

P的状态切换
P的状态切换

从上图我们可以看出 P 执行系统调用时会执行 entersyscall() 函数,另还有一个类似的阻塞函数 entersyscallblock() ,注意两者的区别。当系统调用执行完毕切换回去会执行 exitsyscall() 函数,下面我们看一下这两个函数的实现。

Continue reading

Runtime: 创建一个goroutine都经历了什么?

我们都知道goroutine的在golang中发挥了很大的作用,那么当我们创建一个新的goroutine时,它是怎么一步一步创建的呢?都经历了哪些操作呢?今天我们通过源码来剖析一下创建goroutine都经历了些什么?go version 1.15.6

对goroutine最关键的两个函数是 newproc()newproc1(),而 newproc1() 函数是我们最需要关注的。

函数 newproc()

我们先看一个简单的创建goroutine的例子,找出来创建它的函数。

package main

func start(a, b, c int64) {
	_ = a + b + c
}

func main() {
	go start(7, 2, 5)
}
Continue reading

Runtime: 理解Golang中接口interface的底层实现

接口类型是Golang中是一种非常非常常见的数据类型,每个开发都都很有必要知道它到底是如何使用的,如果了解了它的底层实现就对开发就更有帮助了。

接口的定义

在Golang中 interface 通常是指实现了一 组抽象方法的集合,它提供了一种无侵入式的方式。当你实现了一个接口中指定的所有方法的时候,那么就实现了这个接口,对它的实现并不需要 implements 关键字。

有时候我们称这种模型叫做鸭子模型(Duck typing),维基百科对鸭子模型的定义是 

”If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.“

翻译过来就是 ”如果它看起来像鸭子,像鸭子一样游泳,像鸭子一样嘎嘎叫,那他就可以认为是鸭子“。

Go 不同版本之间interface的结构可能不太一样,但整体都差不多,这里使用的Go版本为 1.15.6。

数据结构

Go 中 interface 在运行时可分 efaceiface 两种数据结构,我们先看一下对它们的定义:

一种是 eface, 表示空接口,指未包含任何方法的接口。如定义一个变量 var x interface{},还有我们经常使用的标准库中的 func Println(a ...interface{}) (n int, err error) {} 都必于 eface 类型,标准库中有许多这类的接口。

Continue reading

认识Golang中的sysmon监控线程

Go Runtime 在启动程序的时候,会创建一个独立的 M 作为监控线程,称为 sysmon,它是一个系统级的 daemon 线程。这个sysmon 独立于 GPM 之外,也就是说不需要P就可以运行,因此官方工具 go tool trace 是无法追踪分析到此线程(源码)。

sysmon

在程序执行期间 sysmon 每隔 20us~10ms 轮询执行一次(源码),监控那些长时间运行的 G 任务, 然后设置其可以被强占的标识符,这样别的 Goroutine 就可以抢先进来执行。

// src/runtime/proc.go

// forcegcperiod is the maximum time in nanoseconds between garbage
// collections. If we go this long without a garbage collection, one
// is forced to run.
//
// This is a variable for testing purposes. It normally doesn't change.
var forcegcperiod int64 = 2 * 60 * 1e9

// Always runs without a P, so write barriers are not allowed.
//
//go:nowritebarrierrec
func sysmon() {
	......

	for {
		if idle == 0 { // start with 20us sleep...
			delay = 20
		} else if idle > 50 { // start doubling the sleep after 1ms...
			delay *= 2
		}
		if delay > 10*1000 { // up to 10ms
			delay = 10 * 1000
		}
		usleep(delay)

		......
	}

	......
}

说明一下,在 sysmon() 函数里有一个sched 变量,这个是全局变量,它整个调度器调度必须使用的一个全局变量,对应结构体为 schedt, 对应字段注释我们上一篇文章里已介绍过。

Continue reading

Golang 的底层引导流程/启动顺序

在Golang中,程序的执行入口为 main() 函数,那么底层又是如何工作的呢? 这个问题的答案我们可以在runtime源码找到。对它的解释主要在 src/runtime/proc.go 文件,下面我们看一下它是如何一步一步开始执行的。go version 1.15.6

在文件头部有一段对 Goroutine scheduler 的介绍,我们先了解一下。

调度器的工作是分发goroutines到工作线程让其运行。一句话指明了调度器的存在意义,就是指挥协调GPM干活。

Continue reading

源码级分析golang的channel实现原理

channel是golang中特有的一种数据结构,通常与goroutine一起使用,下面我们就介绍一下这种数据结构。

channel数据结构

channel最重要的一个结构体就是hchan,我们创建一个channel的时候,实际上是创建了一个下面结构体的实例。

hchan结构体

// src/runtime/chan.go

type hchan struct {
	qcount   uint           // total data in the queue
	dataqsiz uint           // size of the circular queue
	buf      unsafe.Pointer // points to an array of dataqsiz elements
	elemsize uint16
	closed   uint32
	elemtype *_type // element type
	sendx    uint   // send index
	recvx    uint   // receive index
	recvq    waitq  // list of recv waiters
	sendq    waitq  // list of send waiters

	// lock protects all fields in hchan, as well as several
	// fields in sudogs blocked on this channel.
	//
	// Do not change another G's status while holding this lock
	// (in particular, do not ready a G), as this can deadlock
	// with stack shrinking.
	lock mutex
}

字段说明

  • qcount 当前channel中的元素数量
  • dataqsiz 环形队列的大小
  • buf 指向dataqsize的数组指针,只有缓冲chan有效
  • closed 当前channel关闭状态
  • elemsize 存储元素的大小
  • elemtype 存储元素的数据类型
  • sendx 发送操作处理到的索引位置,最大值为数组buf的最大下标值
  • recvx 接收操作处理到的索引位置,最大值为数组buf的最大下标值
  • recvq 接收队列,双向链表,阻塞元素
  • sendq 发送列队,双向链表,阻塞元素
  • lock 锁,,用来保护sudog里的所的字段
hchan struct

其中elemsizeelemtype 表示存储数据的大小和类型;sendxrecvx是指向底层数据的索引位置,表示当前处理的进度位置;recvqsendq 是一个由双向链表实现的队列,它存储的内容是由于队列dataqsize过小,而阻塞的数据。

Continue reading

Runtime:源码解析Golang 的map实现原理

go version 1.15.6

map作为一种常见的 key-value 数据结构,不同语言的实现原理基本差不多。首先在系统里分配一段连接的内存地址作为数组,然后通过对map键进行hash算法(最终将键转换成了一个整型数字)定位到不同的桶bucket(数组的索引位置),然后将值存储到对应的bucket里

map hash算法

理想的情况下是一个bucket存储一个值,即数组的形式,时间复杂度为O(1)。

如果存在键值碰撞的话,可以通过 链表法 或者 开放寻址法 来解决。

Continue reading