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将键移动到申请的内存空间,最后返回键对应的地址sameSizeGrowevacDst结构体只会初始化一个,当哈希表容量翻倍时,一个桶中的元素会被分流到新创建的两个桶中,这两个桶会被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参数并返回错误码2make用于创建 切片,哈希表和管道等内置数据结构
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 中创建出一个新的子上下文,同时还会返回用于取消该上下文的函数,也就是CancelFuncWithDeadline 和 WithTimeout 也都能创建可以被取消的上下文,WithTimeout 只是context 包为我们提供的便利方法,能让我们更方便地创建timeCtxtype 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, WaitGroupGo语言中的互斥锁在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 位置成 1Lock 方法被调用时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循环出发所有由于获取读锁而陷入等待的GoroutineRWMutex 会释放持有的互斥锁让其他的协程能够重新获取读写锁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以及一个互斥锁MutexOnce 结构体对外唯一暴露的方法就是Do,该方法会接受一个入参为空的函数,如果使用atomic.LoadUint32检查到已经执行过函数了,就会直接返回,否则会进入doSlow运行传入的函数doSlow的实现也非常简单,我们先为当前的Goroutine获取互斥锁,然后通过defer关键字将done 成员变量设置成1 并运行传入的函数,无论当前函数是正常运行还是抛出panic,当前方法都会将done设置成1 保证函数不会执行第二次Do方法中传入的函数只会被执行一次,哪怕函数中发生了panicDo方法传入不同的函数时只会执行第一次调用的函数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)已重构,代码开源
当你能力不能满足你的野心的时候,你就该沉下心来学习