学习GO语言,我们可以通过其基本结构,来学习GO语言的一些设计和思想。通过本质去了解GO语言的设计及思想,这样对GO语言的认识也更加彻底和清晰。本文会选取两类结构体来进行介绍,一类是用户开发经常会使用到的切片和哈希表的结构体,另一类是开发中几乎接触不到的GMP模型的结构体来展开介绍。前者可以让我们学习到常见数据结构在编译和运行时的表现,后者则可以通过结构体介绍可以让我们切身感受到为什么GO是一门高性能高并发的语言。
常见数据结构的结构体
在介绍基本数据类型的结构体前,可以先看一下以下的例子,猜测下面的结果会输出什么?
func sliceFunc(a []string, b []string, c *[]string){
a = append(a, "zzz")
a[0]="zzz"
b[0]="zzz"
b = append(b, "zzz")
(*c)[0]="zzz"
*c = append(*c,"zzz")
}
func main() {
a := []string{"aaa", "aaa"}
b := []string{"bbb", "bbb"}
c := []string{"ccc", "ccc"}
sliceFunc(a, b, &c)
fmt.Printf("%v,%v,%v", a, b, c)
}
一、数组和切片的结构体
以下是来自GO 1.15.6源码的数组结构体(后续如无特殊说明,所有源码均来自于版本1.15.6)
// 编译期间的数组和切片类型结构体
// src/cmd/compile/internal/types/type.go
// Array contains Type fields specific to array types.
type Array struct {
Elem *Type // element type
Bound int64 // number of elements; <0 if unknown yet
}
// src/cmd/compile/internal/types/type.go
// Slice contains Type fields specific to slice types.
type Slice struct {
Elem *Type // element type
}
// 运行时的切片类型结构体
// SliceHeader is the runtime representation of a slice.
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
// StringHeader is the runtime representation of a string.
// string实质上是一个char数组
type StringHeader struct {
Data uintptr
Len int
}
从上述的代码中可以发现,无论是数组还是slice,其本质上都是一个个结构体,结构体里保存着实际数据存储的内存空间的指针、长度,slice同时还会存有Cap字段,用于保存容量。由于GO语言的参数传递始终都是传值的。不管是传递结构体还是传指针,将实参传递到函数中的形参上,都会导致值的复制,只是区别在于,前者是复制整个结构体,后者是复制指针。所以会导致不同的差异。
二、哈希表的结构体
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // 当前哈希表中的元素数量
flags uint8
B uint8 // 当前哈希表持有的buckets数量,即len(buckets) == 2^B
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed,随机种子,在创建哈希表时就确定下来了
buckets unsafe.Pointer // 指向当前桶数组[]bmap. may be nil if count==0.
oldbuckets unsafe.Pointer // 扩容时不为空,旧的桶数量是当前桶数量的一半。
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
type mapextra struct {
overflow *[]*bmap //溢出桶
oldoverflow *[]*bmap
// 指向空闲的 overflow bucket 的指针
nextOverflow *bmap //溢出桶指针
}
// A bucket for a Go map.
type bmap struct {
tophash [bucketCnt]uint8 // bucketCnt =8,意味这每个桶里面都能存储8个键值对
}
//在运行期间结构体其实不止包含 `tophash` 字段
//因为哈希表是可以存储不同类型的键值对,所以键值对占据的内存空间大小只能在编译时进行推导。
//bmap中的其他字段在运行时也都是通过计算内存地址的方式访问的,所以它的定义中就不包含这些字段。
//不过我们能根据编译期间的 cmd/compile/internal/gc.bmap`](https://draveness.me/golang/tree/cmd/compile/internal/gc.bmap函数的结构:
type bmap struct {
topbits [8]uint8
keys [8]keytype
values [8]valuetype
pad uintptr
overflow uintptr
}
从哈希表的结构体上我们知道,GO实现的哈希表使用的是拉链法。同时每一个桶对应的对象数组固定位8个,所以每个桶最多能存8个键值对,当桶超过8个键值对时,会创建一个溢出桶,存储在extra结构体中。当哈希表使用了太多的溢出桶时,会触发哈希表扩容(等量扩容)。另外,装载因子超过6.5也会触发扩容(翻倍扩容)。
访问一个元素时,会通过runtime.bucketMask和runtime.tophash返回该键值对所在的桶序号和哈希的高8位,进而找到在对应的桶,然后在bucketloop循环中依次遍历正常桶和溢出桶的高8位,如果相等才去比较键是否相等。
G-M-P模型对应的结构体
GO1.1后,调度器由G-M模型演变成了G-M-P模型
当我们使用go run main.go时:
runtime创建M0,G0
调度器初始化:初始化M0,栈,垃圾回收,创建初始的长度为GOMAXPROCS的P列表(存入全局变量allp中)
runtime.main创建代码的入口main对应的主goroutine,然后放到P的本地队列
启动M0, M0绑定至p上(allp[0])
根据goroutine的栈和调度信息,M0设置运行环境
在M中运行G
当我们使用 go func(){....}()时:
1.创建一个goroutine(G)
2.将G添加到当前P的本地队列或者全局队列中。
3.P中的调度器将G放到与之关联的M中执行。
4.将g添加到当前p的本地队列或者全局队列中。
一、G:Goroutine 结构体
Goroutine只存在于Go语言的运行时,所有的Goroutine调度都由Go语言调度器触发,能减少很多额外开销,所以它比内核态的线程切换更加高效和快速。
它使用runtime.g 结构体表示:
type g struct {
goid int64 //Goroutine的id,该字段不可导出,所以对开发者不可见
stack stack // offset known to runtime/cgo 当前的栈内存范围[stack.lo, stack.hi)
_panic *_panic // 存储panic对应结构体的链表,头节点表示最内侧的panic结构体
_defer *_defer // 存储defer对应结构体的链表,头节点表示最内侧的defer结构体
m *m // 当前Goroutine 占用的线程,可能为空
sched gobuf //存储Goroutine 的调度相关信息的结构体,用于保存和恢复上下文,如 sp 栈指针,pc 程序计数器,ret 系统调用返回值等。
atomicstatus uint32 //Goroutine 的状态。_Gidle, _Grunnable, _Grunning, _Gsyscall, _Gwaiting等
...省略其它字段
}
二、M:Thread 结构体
Go 语言并发模型中的M是操作系统线程。调度器最多可以创建10 000个线程,但是大多数线程不会执行用户代码,最多只会有GOMAXPROCS个活跃线程能够正常运行。
Go语言使用runtime.mt结构体表示操作系统线程:
type m struct {
id int64
g0 *g // 持有调度栈的Goroutine,用来调度其他的Goroutine。会深度参与运行时的调度过程,包括Goroutine的创建、大内存分配和cgo函数的执行等。
curg *g //当前线程上运行的用户Goroutine
p puintptr // attached p for executing go code (nil if not executing go code)。正在运行代码的处理器p。
nextp puintptr //暂存的处理器nextp。
oldp puintptr // the p that was attached before executing a syscall。执行系统调用之前使用线程的处理器oldp。
...省略其它字段
}
三、P:Process 结构体
处理器P能提供线程需要的上下文,负责调度线程上的等待队列,让每个内核线程都能执行多个Goroutine。它也能在Goroutine进行一些I/O操作时及时让出计算资源,提高线程的利用率。
type p struct {
id int32
m muintptr // back-link to associated m (nil if idle)。反向存储着绑定的线程。
// Queue of runnable goroutines. Accessed without lock.
runqhead uint32
runqtail uint32
runq [256]guintptr //本地Goroutine 队列。从队列大小可知,本地队列最多可以存256个Goroutine,当队列满时,会将一半的Goroutine放在全局队列中。
status uint32 // 当前处理器的状态 _Pidle、_Prunning、_Psyscall、_Pgcstop等
link puintptr
schedtick uint32 // incremented on every scheduler call
syscalltick uint32 // incremented on every system call
sysmontick sysmontick // last tick observed by sysmon
// 空闲Gouroutine,如果处理器中不存在空闲Goroutine,就会通过runtime.malg创建一个栈大小(2KB)足够的新结构体
gFree struct {
gList
n int32
}
// 定时器的相关字段。 计时器由处理器的网络轮询器
//(通过系统监控函数runtime.sysmon触发的runtime.netpoll完成)和调度器触发。
timer0When uint64
timersLock mutex //用于保护计时器的互斥锁
timers []*timer //存储计时器的最小四叉堆
numTimers uint32 //处理器中的计时器数量
adjustTimers uint32 //处于timerModifiedEarlier 状态的计时器数量
deletedTimers uint32 //处于timerDeleted状态的计时器数量
timerRaceCtx uintptr
...省略其它字段
}