make
关键字在这个阶段会根据子树的结构被替换成makeslice
或者 makechan
等函数其本质就是引入一个中间层对不同的模块进行解耦,上层的模块就不需要依赖某一个具体的实现!
Go语言中的接口interface
不仅是一组方法,还是一种内置的类型
Go语言中所有的接口的实现都是隐式的
interface{}类型并不表示任意类型, interface{}类型的变量再运行期间的类型只是interface{}
go/src/runtime/runtime2.go#144
iface
结构体type iface struct {
tab *itab
data unsafe.Pointer
}
// layout of Itab known to compilers
// allocated in non-garbage-collected memory
// Needs to be in sync with
// ../cmd/compile/internal/gc/reflect.go:/^func.dumptypestructs.
type itab struct {
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
interfacetype
是对 _type
类型的简单封装hash
对 _type.hash
的拷贝, 它会从 interface
到具体类型的切换时用于 快速判断目标类型和接口中类型是否一致fun
数组其实是一个动态大小的数组,如果数组中内容为空表示 _type
没有实现inter
接口,虽然这是一个大小固定的数组,但是在使用时会直接通过指针获取其中的数据并不会检查数组的边界,所以该数组中保存的元素数量是不确定的interface{}
类型eface
结构体type eface struct {
_type *_type
data unsafe.Pointer
}
_type
类型有点复杂.. 不看了..运行期间选择具体的多态操作执行的过程,在Go语言中,对于一个接口类型的方法调用,会在运行期间决定具体调用该方法的哪个实现
动态派发会出现一些消耗,但一般项目中不可能只存在动态派发的调用,荔港南湾,如果开启默认的编译器优化,动态派发开销还会降低,所以对整体性能影响很小
结构体指针换成结构体,消耗区别有点大,原因是 Go 函数调用是值传递,会出现参数拷贝,所以对于大结构体,参数拷贝会消耗非常多资源,所以应该用指针来传递大结构体
Array
包含两个结构,一个是元素类型Elem
, 另一个是数组的大小上限Bound
,这两字段组成了数组类型Array
类型,不过Bound = -1
后面会推到该数组大小[...]T
类型的声明不是在运行是被推导的,会在类型检查期间就被推断出正确的数组大小[...]T{1, 2, 3}
与[3]T{1, 2, 3}
运行的时候 是等价的,理由如上Slice{Elem: elem}
range
遍历切片时也是在编译期间被转换成了形式更简单的代码type SliceHeader struct {
Data uintptr // 指向数组的指针
Len int // 当前切片长度
Cap int // 切片的容量
}
slice := []int{1, 2, 3}
append
会根据inplace
在中间代码生成阶段转换不同流程,一种是追加之后,不需要赋值回原有的变量append(slice, 1, 2, 3)
,一种是需要赋值给原有的变量slice = apennd(slice, 1, 2, 3)
growslice
对切片进行扩容并将新的元素依次加入切片并创建新的切片
func growslice(et *_type, old slice, cap int) slice {
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
copy(a, b)
会在编译期间转换成slicecopy
函数memmove
或者数组指针的方式将整块内存中的内容拷贝到目标的内存区域,所以大切片拷贝需要注意性能影响,不过比一个个的复制要有更好的性能哈希表示键值之间隐射关系
哈希函数
开放寻址法
拉链法
type hmap struct {
count int // 用于记录当前哈希表元素数量,这个元素让我们不在需要去遍历整个哈希表来获取长度
flags uint8
B uint8 // 表示当前哈希表持有的 `buckets` 数量,因为哈希表扩容是以2倍进行的,所以这里会使用对数来存储, 简单理解成 `len(buckets) == 2^B`
noverflow uint16
hash0 uint32 // 哈希的种子,这个值会在调用哈希函数时作为参数传入进去,主要作用是为哈希函数的结果引入一定的随机性
buckets unsafe.Pointer
oldbuckets unsafe.Pointer // 哈希在扩容时用于保存之前的 `buckets`的字段,它的大小是当前`buckets`的一半
nevacuate uintptr
extra *mapextra
}
maplit
进行初始化addMapEntries
将结构体转成单独的键值对,比如 hash[“a”] = 1, hash[“b”] = 2
hash := make(map[string]int, 26)
vstatk := []string{"1", "2", "3", ... , "26"}
vstatv := []int{1, 2, 3, ... , 26}
for i := 0; i < len(vstak); i++ {
hash[vstatk[i]] = vstatv[i]
}
make
转换成makemap
来创建哈希表B
算出需要创建的桶数量,在内存里分配一片连续空间用于存储数据,创建过程中,还会创建一些用于保存溢出数据的桶,数量 2^(B-4)个类型检查阶段,类似hash[key]
的OINDEX
操作都会被转换成OINDEXMAP
操作,中间代码生成阶段,在walkexpr
中将这些OINDEXMAP
转成
v := hash[key] // => v := *mapaccess1(maptype, hash, &key)
v, ok := hash[key] // => v, ok := mapaccess2(maptype, hash, &key)
当接收参数就一个时,使用mapaccess1
函数,如果多加一个是否存在的布尔值就会使用mapaccess2
在这个函数中我们首先会通过哈希表设置的哈希函数、种子获取当前键对应的哈希,再通过 bucketMask 和 add 函数拿到该键值对所在的桶和哈希最上面的 8 位数字,这 8 位数字最终就会与桶中存储的 tophash 作对比,每一个桶其实都存储了 8 个 tophash,就是编译期间的 topbits 字段,每一次都会与桶中全部的 8 个 uint8 进行比较,这 8 位的 tophash 其实就像是一级缓存,它存储的是哈希最高的 8 位,而选择桶时使用了桶掩码使用的是最低的几位,这种方式能够帮助我们快速判断当前的键值对是否存在并且减少碰撞,每一个桶都是一整片的内存空间,当我们发现某一个 topbits 与传入键的 tophash 匹配时,通过指针和偏移量获取哈希中存储的键并对两者进行比较,如果相同就会通过相同的方法获取目标值的指针并返回。另一个同样用于访问哈希表中数据的 mapaccess2 函数其实只是在 mapaccess1 的基础上同时返回了一个标识当前数据是否存在的布尔值
hashGrow
对当前哈希表进行扩容loadFactorNum
和 loadFactDen
两个参数决定的,前者在 Go 源代码中的定义是 13 后者是 2,所以装载因子就是 6.5newoverflow
创建一个新的桶或者使用hmap
预先在noverflow
中创建好的桶来保存数据,新创建的桶的指针会被追加到已有桶中,与此同时,溢出桶的创建会增加哈希表的noverflow
计数器eypedmemmove
将键移动到申请的内存空间,最后返回键对应的地址sameSizeGrow
evacDst
结构体只会初始化一个,当哈希表容量翻倍时,一个桶中的元素会被分流到新创建的两个桶中,这两个桶会被evacDst
数组引用delete
关键字,将某一个键对应的元素从哈希表中删除,无论该键对应的值是否存在,这个内建的函数都不会返回任何的结果memclrHasPointers
或者 memclrNoHeapPointers
函数完成键值对的删除delete
在类型检查阶段被转换成ODELETE
操作,然后在 SSA中间代码生成
时被转换成mapdelete
函数簇tophash
就成了一级缓存帮助哈希快速遍历桶中元素SRODATA
,但这只是表示这个字符串会被分配到只读的内存空间并且这段内存不会被修改,但是运行时,依然可以将这段内存拷贝到其他的堆或者栈上,同时将变量的类型修改成 []byte
,在修改之后通过类型转换变成string
,如果想直接修改string
类型变量的内存空间,是不支持的!StringHeader
结构体进行表示, 在运行时包的内部其实有一个私有的结构stringHeader
, 它有着相同的结构,只是用于存储数据的Data
字段使用了unsafe.Pointer
类型type StringHeader struct {
Data uintptr
Len int
}
\
符号避免编译器的解析错误\
来escape
双引号\n
str := "start
end"
"
,写JSON
或者其他数据时候非常方便+
符号,编译器在检查阶段将OADD
节点转换成OADDSTR
类型的节点,然后在SSA中间代码生成的阶段调用addstr
函数addstr
函数在编译期间合选择合适的函数对字符串进行拼接,如果拼接字符串小于或者等于5个,那么活直接调用concatstring{2,3,4,5}
等一系列函数,如果超过5个就会直接选择concatstrings
传入一个数组切片concatstrings
,这个函数会先对传入的切片参数进行遍历,首先会过滤空字符串比国内获取拼接后的字符串长度string(bytes)
会在编译期间转换成slicebytetostring
的函数调用stringtoslicebyte
rawbyteslice
创建一个新的字节切片,copy
关键字会将字符串中的内容拷贝到新的字节数组中for
和 for...range
经过优化后,变成一样
结果是????
func main() {
arr := []int{1, 2, 3}
for _, v := range arr {
arr = append(arr, v)
}
fmt.Println(arr)
}
func main() {
arr := []int{1, 2, 3}
newArr := []*int{}
for _, v := range arr {
newArr = append(newArr, &v)
}
for _, v := range newArr {
fmt.Println(*v)
}
}
结果是 , 如何避免
for 经典循环
for…range 范围循环 编译器会在编译期间将带有range
的循环变成普通的经典循环,这个过程发生在 SSA中间代码 阶段,所有的range
都会被walkrange
函数转换成只包含基本表达式的语句,不包含任何复杂的结构
range
循环,Go语言都会在编译期间将原切片或者数组赋值给一个新的变量ha
,在赋值的过程中其实就发生了拷贝,所以我们遍历的切片其实已经不是原有的切片变量了!range
循环时,Go语言会额外创建一个新的v2
变量存储切片中的元素,循环中使用的这个变量v2会在每一次迭代中都被重新赋值,在赋值时也发生了拷贝, 所以我们想要访问数组中元素所在的地址,不应该直接获取range
返回的v2
变量的地址&v2
,想要解决这个问题应该使用&a[index]
这种方式获取数组中元素对应的地址fastrand
函数)选择开始的位置,然后依次遍历桶中的元素,桶中元素如果被遍历完,就会遍历当前桶对应的溢出桶,溢出桶都遍历结束之后才会遍历哈希中下一个桶,直到所有的桶都被遍历完rune
,我们在遍历字符串时拿到的值都是rune
类型的变量<-ch
从管道中取出等待处理的值,这个操作会调用chanrecv2
并阻塞当前的协程,当chanrecv2
返回时会根据hb
来判断当前的值是否存在,如果不存在就意味着当前的管道已经被关闭了,在正常情况下都会为v1
赋值并清除hv1
中的数据,然后会陷入下一次的阻塞等待接受新的数据defer
实现是由编译器和运行时共同完成的func main() {
{
defer fmt.Println("defer runs")
fmt.Println("block ends")
}
fmt.Println("main ends")
}
defer
只会在当前函数和方法返回之前被调用type Test struct {
value int
}
func (t Test) print() {
println(t.value)
}
func main() {
test := Test{}
defer test.print()
test.value += 1
}
稍微改动一下
type Test struct {
value int
}
func (t *Test) print() {
println(t.value)
}
func main() {
test := Test{}
defer test.print()
test.value += 1
}
test
的指针,上面那个复制的是结构体,这段是复制的指针,修改test.value
时,defer
捕获的指针其实就能够访问到修改后的变量了type _defer struct {
siz int32
started bool
sp uintptr
pc uintptr
fn *funcval
_panic *_panic
link *_defer
}
sp
和pc
分别指向了栈指针和调用方的程序计数器,fn
存储的就是向defer
关键字中传入的函数defer
关键字在编译期间的SSA阶段才被stmt
函数处理的,中间详情不表defer
关键字都会被转换成deferproc
,这个函数里会为defer
创建一个新的_defer
结构体并设置它的fn
,pc
和sp
参数,并将defer
相关的函数都拷贝到紧挨着结构体的内存空间中defer
关键字会在编译阶段被转换成deferproc
的函数并在函数返回之前插入deferreturn
指令,在运行期间,每一次deferproc
的调用都会将一个新的_defer
结构体追加到当前Goroutine持有的链表头,而deferreturn
会从Goroutine中取出_defer
结构并以此执行,所有的_defer
结构执行成功之后当前函数才返回!defer
函数,执行成功返回到调用方panic
类似,执行所有的defer
函数并返回到它的调用方,这个过程会一直进行到当前的Goroutine的调用栈不包含任何的函数,这时整个程序才会崩溃panic
导致的恐慌
状态其实可以被defer
中的recover
中止,recover
是一个只在defer
中能够发挥作用的函数,在正常的控制流程中,recover
会直接返回nil
并没有任何的作用,如果当前的Goroutine发生了恐慌
,recover
就能够捕获到panic
抛出的错误并阻止恐慌
的继续传播type _panic struct {
argp unsafe.Pointer // 指向`defer`调用时参数的指针
arg interface{} // 调用`panic`时传入的参数
link *_panic // 指向更早调用的`_panic`结构
recovered bool // 当前的`_panic`是否被`recover` 恢复
aborted bool // 表示当前的`panic`是否被强行终止
}
panic
调用所在的Goroutine协程_panic
结构体_defer
结构体_defer
存在,调用reflectcall
执行_defer
中的代码_defer
结构设置到Goroutine上并返回到3fatalpanic
中止整个程序(会在中止整个程序之前可能会通过printpanics
打印出全部的panic
消息以及调用时传入的参数)panic
和recover
分别转换成gopanic
和gorecover
函数,同时将defer
转换成deferproc
函数并在调用defer
的函数和方法末尾增加deferreturn
的指令gopanic
方法时,会从当前Goroutine中取出_defer
的链表并通过reflectcall
调用用于收尾的函数reflectcall
调用时遇到了gorecover
就会直接将当前的 _panic.recovered
标记成true
并返回panic
传入的参数(在这时recover
就能够获取到panic
的信息)
gopanic
会从_defer
结构体中取出程序计数器pc
和栈指针sp
并调用recovery
方法进行恢复recovery
会根据传入的pc
和sp
跳转到deferproc
函数deferproc
的返回值不为0
,这时就会直接跳到deferreturn
函数中并恢复到正常的控制流程(依次执行剩余的defer
并正常退出)gorecover
就会一次遍历所有的_defer
结构,并在最后调用fatalpanic
中止程序,打印panic
参数并返回错误码2
make
用于创建 切片,哈希表和管道等内置数据结构
OMAKESLICE
,OMAKEMAP
,OMAKECHAN
三种不同类型的节点new
用于分配并创建一个指向对应类型的指针
SSA 代码生成
阶段经过 callnew
函数的处理,如果请求创建的类型大小是0,那么就会返回一个表示空指针的zerobase
变量,在遇到其他情况会将关键字转换成newobject
生存
,不会初始化在当前函数的栈中并随着函数调用的结束而被销毁Context
主要作用就是在 不同的 Goroutine 之间同步请求特定的数据,取消信号以及处理请求的截至日期Context
都会从最顶层的 Goroutine 一层一层传递到最下层,如果没有 Context
,上层执行操作出现错误时,下层不会收到错误而会继续执行下去type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Deadline
方法需要返回当前Context
被取消的时间,也就是完成工作的截至日期Done
方法需要返回一个 Channel, 这个 Channel 会在当前工作完成或者上下文被取消之后关闭, 多次调用 Done
方法会 返回同一个 ChannelErr
方法会返回当前 Context
结束的原因, 它只会在Done
返回的 Channel 被关闭才会返回非空的值
Context
被取消就会返回 Canceled
错误Context
超时就会返回 DeadlineExceeded
错误Value
方法会从 Context
中返回键对应的值,对于同一个上下文来说,多次调用Value
并传入相同的Key
会返回相同的结果,这个功能可以用来传递特定的数据Backgroud
和 TODO
方法在某种层面上看也是互为别名,两者没有太大区别,不过context.Background()
是上下文中最顶层的默认值,所有其他的上下文都应该从context.Background()
演化出来context.TODO()
,在多数情况下如果函数没有上下文作为入参,我们往往都会使用context.Background()
作为起始的Context
向下传递WithCancel
方法能从 Context
中创建出一个新的子上下文,同时还会返回用于取消该上下文的函数,也就是CancelFunc
WithDeadline
和 WithTimeout
也都能创建可以被取消的上下文,WithTimeout
只是context
包为我们提供的便利方法,能让我们更方便地创建timeCtx
type timer struct {
tb *timersBucket
i int
when int64
period int64
f func(interface{}, uintptr)
arg interface{}
seq uintptr
}
timer
就是 Golang 定时器内部表示,每一个timer
其实都存在堆中
tb
就是用于存储当前定时器的桶
i
是当前定时器在堆中的索引,可以通过这两个变量找到当前定时器在堆中的位置
when
表示当前定时器(Timer) 被唤醒的时间
period
表示两次被唤醒的间隔,每当定时器被唤醒时都会调用f(args,now)
函数并传入args
和当前时间作为参数
这里的timer
作为一个私有结构体其实只是定时器的运行时表示,time
包对外暴露的定时器是如下结构
type Timer struct {
C <-chan Time
r runtimeTimer
}
Timer
定时器必须通过NewTimer
或者 AfterFunc
函数进行创建,其中的runtimeTimer
其实就是上面的timer
结构体, 当定时器失效时,失效的时间就会被发送给当前定时器持有的ChannelC
, 订阅管道中消息的Goroutine就会接收到当前定时器失效的时间
time
包对外提供了两种创建定时器的方法
NewTimer
接口创建用于通知触发时间的Channel,调用 startTimer
方法并返回一个创建指向Timer
结构体的指针AfterFunc
也提供了相似的结构,与上面不同的是,它只会在定时器到期时调用传入的方法startTimer
是创建定时器的入口,所有的定时器的创建和重启基本上都需要这个函数addTimer
函数,首先通过assignBucket
方法为当前定时器选择一个timersBucket
桶,根据当前的Goroutine所在处理器P的id选择一个合适的桶,随后调用addTimerLocked
方法将当前定时器加入桶中addtimerLocked
会先将最新加入的定时器加到队列的末尾,随后调用siftipTimer
将当前定时器与四叉树(或者四叉堆)中的父节点进行比较,保证父节点的到期时间一定小于子节点timerproc
中的一个双层for
循环控制的,外层的for
循环主要负责对当前的Goroutine
进行控制,它不仅会负责锁的获取和释放,还会在合适的时机触发当前Goroutine的休眠notetsleepg
方法陷入休眠等待最近定时器的到期preiod > 0
就会设置下一次会触发定时器的时间并将当前定时器向下移动到对应位置preios <= 0
就会将当前定时器从四叉树中移除NewTimer
创建的定时器,传入的函数时sendTime
,它会将当前时间发送到定时器持有的Channel中,而使用AfterFunc
创建的定时器,在内层循环中调用的函数就会是调用方法传入的函数了timeSleep
会创建一个新的timer
结构体,在初始化的过程中我们会传入当前Goroutine 应该被唤醒的时间以及唤醒时需要调用的函数goroutineReady
,随后会调用 goparklock
将当前GOroutine陷入休眠状态,当定时器到期时也会调用 goroutineReady
方法唤醒当前的Goroutinetime.Sleep
方法其实只是创建了一个会在到期时唤醒当前Goroutine的定时器并通过goparkunlock
将当前的协程陷入休眠状态等待定时器触发的唤醒time
包中还提供了用于多次通知的Ticker
计时器,计时器中包含了一个用于接受通知的Channel 和一个定时器,这个两个字段组成了用于连续多次触发事件的计时器NewTicker
方法显示的创建Ticker
计时器指针,另一种可以直接通过Tick
方法获取一个会定期发送消息的ChannelNewTicker
方法开启的计时器都需要在不需要使用时调用Stop
进行关闭,如果不显示调用Stop
方法,创建的计时器就没有办法被垃圾回收,而通过Tick
创建的计时器由于只对外提供了Channel,所以是一定没有办法关闭的,我们一定要谨慎使用这一接口创建计时器sync
包中提供了用于同步的一些基本原语,包括常见的互斥锁Mutex
与读写互斥锁 RWMutex
以及 Once
, WaitGroup
Go语言中的互斥锁在sync
中, 由state
和sema
组成, state
表示 当前互斥锁的状态, 而sema
真正用于控制锁状态的信号量, 这两个加起来只占8字节空间的结构体就表示了Go语言中的互斥锁
type Mutex struct {
state int32
sema uint32
}
互斥锁的状态是用int32
来表示的,但是锁的状态并不是互斥的,它的最低三位分别表示mutexLocked
,mutexWoken
,mutexStarving
,剩下的位置都用来表示当前有多少个Goroutine等待互斥锁被释放
互斥锁在被创建出来时,所有的状态位的默认值都是0
,当互斥锁被锁定时,mutexLocked
就会被置成1
,当互斥锁被在正常模式下被唤醒mutexWoken
就会被置成1
,mutexStarving
用于表示当前的互斥锁进入了状态,最后的几位是在当前互斥锁上等待的Goroutine个数
1.9
版本引入的特性,主要功能就是保证互斥锁的获取的公平性
Mutex
的一个优化饥饿模式不会进入进入自旋,那么如果是正常模式转成饥饿模式,自旋还有么??????
Mutex
的加锁是靠Lock
方法完成的,最新的 Go语言源代码中已经将Lock
方法进行了简化,方法的主干只保留了最常见,简单并且快速的情况,当锁的状态是 0
时直接将mutexLocked
位置成 1
Lock
方法被调用时Mutex
的状态不是 0
时就会进入 lockSlow
方法尝试通过自旋或者其他方法等待锁的释放并获取互斥锁Mutex
互斥锁中,只有在普通模式下才可能进入自旋,除了模式的限制之外, runtime_canSpin
方法中会判断当前方法是否可以进入自旋,进入自旋的条件非常苛刻
P
并且处理的运行队列是空的runtime_SemacquireMutex
方法主要作用就是通过Mutex
的使用互斥锁中的信号量保证资源不会被两个Goroutine获取,从这里我们就能看出Mutex
其实就是对更底层的信号量进行封装,对外提供更加易用的API,runtime_SemacquireMutex
会在方法中不断调用 goparkunlock
将当前 Goroutine陷入休眠等待信号量可以被获取,Lock
方法的剩余代码也会继续执行下去了,当前互斥锁处于饥饿模式时,如果该Goroutine是队列中最后的一个Goroutine 或者等待锁的时间小于 starvationThresho1dNs(1ms)
当前Goroutine 就会直接获得互斥锁并且从饥饿模式中退出并获得锁Unlock
方法会直接使用atomic
包提供的AddInt32
,如果返回的新状态不等于 0
就会进入 unlockSlow
方法unlockSlow
方法首先会对锁的状态进行校验,如果当前互斥锁已经被解锁过了就会直接抛出异常sync: unlock of unlocked mutex
中止当前程序,在正常情况下会根据当前互斥锁的状态是正常模式还是饥饿模式进入不同的分支!runtime_Semrelease
方法直接将当前锁交给下一个正在正在尝试获取锁的等待者,等待者会在被唤醒之后设置mutexLocked
状态,由于此时仍然处于mutexStarving
,所以新的Goroutine也无法获得锁runtime_Semrelease
唤醒对应的 Goroutine并移交锁的所有权mutexLocked
加锁mutexLocked
并且在普通模式下工作,就会进入自旋,执行30次PAUSE
指令消耗CPU时间等待锁的释放runtime_SemacquireMutex
方法将调用Lock
的 Goroutine 切换至休眠状态,等待持有信号量的Goroutine唤醒当前协程Unlock
会直接抛出异常mutexLocked
标志位runtime_Semrelease
唤醒对应的Goroutine读写互斥锁在Go语音中的实现是 RWMutex
,其中不仅包含一个互斥锁,还持有两个信号量,分别用于写等待读和读等待写
type RWMutex struct {
w Mutex
writerSem uint32
readerSem uint32
readerCount int32
readerWait int32
}
readerCount
存储了当前正在执行的读操作的数量,最后的readerWait
表示当写操作被阻塞时等待的读操作个数
atomic.AddInt32
方法为readerCount
加一,如果该方法返回了负数说明当前有Goroutine获得了写锁,当前Goroutine就会调用runtime_SemacquireMutex
陷入休眠等待唤醒readerCount
加一后返回,当Goroutine想要释放读锁时会调用RUnlock
方法,该方法会减少正在读资源的readerCount
,当前方法如果遇到了返回值小于零的情况,说明有一个正在进行的写操作,在这时就应该通过rUnlockSlow
方法减少当前写操作等待的读操作数readerWait
并在所有都被释放之后出发写操作的信号量writerSem
,writerSem
被触发之后,尝试获取读写锁的进程就会被唤醒并获得锁Lock
方法了,在Lock
方法中首先调用了读写互斥锁持有的Mutex
的Lock
方法保证其他获取读写锁的Goroutine 进入等待状态,随后的atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders)
其实是为了堵塞后续的读操作runtime_SemacquireMutex
进入休眠状态,等待读锁释放时触发writerSem
信号量将当前协程唤醒atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
变回正数并通过for循环出发所有由于获取读锁而陷入等待的Goroutine
RWMutex
会释放持有的互斥锁让其他的协程能够重新获取读写锁readerSem
- 读写锁释放时通知由于获取读锁等待的GoroutinewriterSem
- 读锁释放时通知由于获取读写锁等待的Goroutinew
互斥锁 - 保证写操作之间的互斥readerCount
- 统计当前进行读操作的协程数,触发写锁时会将其减少rwmutexMaxReaders
阻塞后续的读操作readerWait
- 当前读写锁等待的进行读操作的协程数,在出发Lock
之后的每次RUnlock
都会将其减一,当它归零时该Goroutine就会获得读写锁Unlock
时首先通知所有的读操作,然后才会释放持有的互斥锁,这样能够保证读操作不会被连续的写操作饿死
WaitGroup
是Go语言sync
包中比较常见的同步机制,它可以用于等待一系列的Goroutine的返回,一个比较常见的使用场景是批量执行RPC或者调用外部服务
通过WaitGroup
我们可以在多个Goroutine之间非常轻松的同步信息,原本顺序执行的代码也可以在多个Goroutine中并发执行,加快了程序处理的速度
type WaitGroup struct {
noCopy noCopy
state1 [3]uint32
}
noCopy
的主要作用就是保证WaitGroup
不会被开发者通过再赋值的方式进行拷贝,进而导致一些诡异的行为
copyLock
包就是一个用于检测类似错误的分析器,它的原理就是在 编译期间 检查被拷贝的变量中是否包含的noCopy
或者sync
关键字
除了noCopy
之外,WaitGroup
结构体中还包含一个总共占用12字节大小的数组,这个数组中会存储当前结构体持有的状态和信号量,在64位与32位机器上表现也非常不同
WaitGroup
提供了私有方法 state
能够帮助我们从state1
字段中取出它的状态和信号量
WaitGroup
对外暴露的接口只有三个Add
,Wait
和Done
,其中Done
只是调用了wg.Add(-1)
Add
方法的主要作用就是更新WaitGroup
中持有的计数器counter
,64位状态的高32位,虽然Add
方法传入的参数可以为负数,但是一个WaitGroup
的计数器只能是非负数,当调用Add
方法导致计数器归零并且还有等待的Goroutine时,就会通过runtime_Semrelease
唤醒处于等待状态的所有Goroutine
另一个WaitGroup
的方法Wait
就会在当前计数器中保存的数据大于0 时修改等待Goroutine 的个数waiter
并调用 runtime_Semacquire
陷入睡眠状态
陷入睡眠的Goroutine
就会等待Add
方法在计数器为 0 时唤醒
Add
不能在和Wait
方法在 Goroutine 中并发调用,一旦出现就会造成程序崩溃WaitGroup
必须在 Wait
方法返回之后才能被重新使用Done
只是对Add
方法的简单封装,我们可以向Add
方法传入任意负数(需要保持计数器非负)快速将计数器归零以唤醒其他等待的GoroutineWaitGroup
计数器的归零,这些Goroutine也会被同时唤醒Once
保证在Go程序运行期间Once
对应的某段代码只会执行一次
type Once struct {
done uint32
m Mutex
}
sync
包中的结构体,Once
有着非常简单的数据结构,每一个Once
结构体都只包含一个用于标识代码块是否被执行过的done
以及一个互斥锁Mutex
Once
结构体对外唯一暴露的方法就是Do
,该方法会接受一个入参为空的函数,如果使用atomic.LoadUint32
检查到已经执行过函数了,就会直接返回,否则会进入doSlow
运行传入的函数doSlow
的实现也非常简单,我们先为当前的Goroutine获取互斥锁,然后通过defer
关键字将done
成员变量设置成1 并运行传入的函数,无论当前函数是正常运行还是抛出panic
,当前方法都会将done
设置成1 保证函数不会执行第二次Do
方法中传入的函数只会被执行一次,哪怕函数中发生了panic
Do
方法传入不同的函数时只会执行第一次调用的函数Go语言在标准库中提供的Cond
其实是一个条件变量,通过Cond
我们可以让一系列的 Goroutine 都在触发某个事件或者条件时才被唤醒,每一个Cond
结构体都包含一个互斥锁L
type Cond struct {
noCopy noCopy
L Locker
notify notifyList
checker copyChecker
}
Cond
结构体中包含noCopy
和 copyChecker
两个字段,前者用于保证Cond
不会再编译期间拷贝,后者保证在运行期间发生拷贝会直接panic
,持有的另一个锁L
其实是一个接口Locker
,任意实现Lock
和Unlock
方法的结构体都可以作为NewCond
方法的参数
结构体中的最后的变量notifyList
其实也就是为了实现Cond
同步机制,该结构体其实就是一个Goroutine的链表
Cond
对外暴露的Wait
方法会将当前Goroutine陷入休眠状态,它会先调用runtime_notifyListAdd
将等待计数器 +1,然后解锁并调用 runtime_notifyListWait
等待其他Goroutine的唤醒
notifyListWait
方法的主要作用就是获取当前的Goroutine并将它追加到 notifyList
链表的最末端
除了将当前Goroutine追加到链表的最末端之外,我们还会调用goparkunlock
陷入睡眠状态,该函数也是在Go语音切换Goroutine 时经常会使用的方法,它会直接让当前处理器的使用权并等待调度器的唤醒
Cond
对外提供的Signal
和 Broadcast
方法就是用来唤醒调用Wait
陷入休眠的Goroutine,前者会唤醒队列最前面的Goroutine,后者会唤醒队列中全部的Goroutine
notifyListNotifyAll
方法会从链表中取出全部的Goroutine并为他们依次调用 readyWithTime
, 该方法会通过goready
将目标的Goroutine 唤醒
虽然它会唤醒全部的Goroutine,但是这里唤醒的顺序其实还是按照加入队列的先后顺序,先加入的会先被goready
唤醒,后加入的Goroutine可能就需要等待调度器的调度
notifyListNotifyOne
函数就只会从 sudog
构成的链表中满足 sudog.ticket == l.notify
的Goroutine并通过readyWithTime
唤醒
在一般情况下我们会选择在不满足特定条件时调用Wait
陷入休眠,当某些Goroutine检测到当前满足了唤醒的条件,就可以选择使用Signal
通过一个或者 Broadcast
通知全部的Goroutine当前条件已经满足,可以继续完成工作了
Mutex
相比,Cond
还是一个不被所有人都清楚和理解的同步机制,它提供了类似队列的FIFO的等待机制,同时也提供了Signal
和 Broadcast
两种不同的唤醒方法,相比于使用for{}
忙碌等待,使用Cond
能够在遇到长时间条件无法满足时将当前处理器让出的功能,如果我们合理使用还是能够在一些情况下提升性能Wait
方法在调用之前一定要使用L.Lock
持有该资源,否则会发生panic
导致程序崩溃Signal
方法唤醒Goroutine都是队列最前面 等待最久的GoroutineBroadcast
虽是广播通知等待的Goroutine,但是真正被唤醒时也是按照一定顺序的本站(PHP --> Golang)已重构,代码开源
当你能力不能满足你的野心的时候,你就该沉下心来学习