├── Go Channel.md ├── Go Defer.md ├── Go Map.md ├── Go Select.md ├── Go Semaphore.md ├── Go Slice.md ├── Go Sync.md ├── Go Sync——Mutex.md ├── Go interface 反射.md ├── Go interface.md ├── Go panic 和 recover.md ├── Go 内存一致性模型.md ├── Go 内存管理.md ├── Go 协程调度——PMG 调度细节分析.md ├── Go 协程调度——基本原理与初始化.md ├── Go 垃圾回收.md ├── Go 系统调用.md ├── Go 网络调用 netpoll.md ├── README.md └── img ├── PMG1.png ├── PMG2.png ├── PMG3.png ├── _type.png ├── arenas.png ├── arenas2.jpg ├── arenas2.png ├── arenas3.jpg ├── arenas4.jpg ├── bucket.png ├── cat.png ├── cat1.png ├── cat3.png ├── copy1.jpg ├── copy2.jpg ├── cycle.jpg ├── duck.png ├── duck2.png ├── duck3.png ├── duck4.png ├── duck5.png ├── duck6.png ├── falseshare.png ├── gc1.png ├── gen1.jpg ├── gen2.jpg ├── hashgrow.png ├── hashgrow1.png ├── hmap.png ├── interface.png ├── interface1.png ├── interface3.png ├── map.png ├── marksweep.jpg ├── marksweep1.jpg ├── mem.jpg ├── mem.png ├── mesi.png ├── mesi2.png ├── mesi3.jpg ├── mesi4.jpg ├── methodset.png ├── mheap.jpg ├── mspan.jpg ├── mutex.png ├── preem.jpg ├── span1.jpg ├── stackmap.png ├── stackmap1.png ├── tiny1.png ├── tiny2.png ├── volatile.png ├── volatile1.png ├── volatile3.png ├── volatile4.png ├── volatile5.png ├── waiting.png ├── writeb.jpg └── writeb2.jpg /Go Channel.md: -------------------------------------------------------------------------------- 1 | # Go Channel 2 | 3 | [toc] 4 | 5 | ## hchan 数据结构 6 | 7 | ``` 8 | // channel 在 runtime 中的结构体 9 | type hchan struct { 10 | // 队列中目前的元素计数 11 | qcount uint // total data in the queue 12 | // 环形队列的总大小,ch := make(chan int, 10) => 就是这里这个 10 13 | dataqsiz uint // size of the circular queue 14 | // void * 的内存 buffer 区域 15 | buf unsafe.Pointer // points to an array of dataqsiz elements 16 | 17 | // sizeof chan 中的数据 18 | elemsize uint16 19 | // runtime._type,代表 channel 中的元素类型的 runtime 结构体 20 | elemtype *_type // element type 21 | 22 | // 是否已被关闭 23 | closed uint32 24 | // 发送索引 25 | sendx uint // send index 26 | // 接收索引 27 | recvx uint // receive index 28 | 29 | // 接收 goroutine 对应的 sudog 队列 30 | recvq waitq // list of recv waiters 31 | // 发送 goroutine 对应的 sudog 队列 32 | sendq waitq // list of send waiters 33 | 34 | 35 | lock mutex 36 | } 37 | 38 | ``` 39 | 40 | ## 初始化 41 | 42 | - 如果当前 Channel 中不存在缓冲区,那么就只会为 hchan 分配一段内存空间; 43 | - 如果当前 Channel 中存储的类型不是指针类型,就会直接为当前的 Channel 和底层的数组分配一块连续的内存空间; 44 | - 在默认情况下会单独为 hchan 和缓冲区分配内存; 45 | 46 | ``` 47 | func makechan(t *chantype, size int) *hchan { 48 | elem := t.elem 49 | 50 | // 如果 hchan 中的元素不包含有指针,那么就没什么和 GC 相关的信息了 51 | var c *hchan 52 | 53 | switch { 54 | case size == 0 || elem.size == 0: 55 | // 如果 channel 的缓冲区大小是 0: var a = make(chan int) 56 | // 或者 channel 中的元素大小是 0: struct{}{} 57 | // Queue or element size is zero. 58 | c = (*hchan)(mallocgc(hchanSize, nil, true)) 59 | // Race detector uses this location for synchronization. 60 | c.buf = unsafe.Pointer(c) 61 | case elem.kind&kindNoPointers != 0: 62 | // Elements do not contain pointers. 63 | // Allocate hchan and buf in one call. 64 | // 通过位运算知道 channel 中的元素不包含指针 65 | // 占用的空间比较容易计算 66 | // 直接用 元素数*元素大小 + channel 必须的空间就行了 67 | // 这种情况下 gc 不会对 channel 中的元素进行 scan 68 | c = (*hchan)(mallocgc(hchanSize+uintptr(size)*elem.size, nil, true)) 69 | c.buf = add(unsafe.Pointer(c), hchanSize) 70 | default: 71 | // Elements contain pointers. 72 | // 和上面那个 case 的写法的区别:调用了两次分配空间的函数 new/mallocgc 73 | c = new(hchan) 74 | c.buf = mallocgc(uintptr(size)*elem.size, elem, true) 75 | } 76 | 77 | c.elemsize = uint16(elem.size) 78 | c.elemtype = elem 79 | c.dataqsiz = uint(size) 80 | 81 | return c 82 | } 83 | 84 | ``` 85 | 86 | ## send 发送 87 | 88 | - 直接发送:如果目标 Channel 没有被关闭并且已经有处于读等待的 Goroutine,那么chansend 函数会通过 dequeue 从 recvq 中取出最先陷入等待的 Goroutine 并直接向它发送数据; 89 | - 缓冲区:向 Channel 中发送数据时遇到的第二种情况就是创建的 Channel 包含缓冲区并且 Channel 中的数据没有装满. 在这里我们首先会使用 chanbuf 计算出下一个可以放置待处理变量的位置,然后通过 typedmemmove 将发送的消息拷贝到缓冲区中并增加 sendx 索引和 qcount 计数器,在函数的最后会释放持有的锁。 90 | - 阻塞发送: 91 | - 调用 getg 获取发送操作时使用的 Goroutine 协程; 92 | - 执行 acquireSudog 函数获取一个 sudog 结构体并设置这一次阻塞发送的相关信息,例如发送的 Channel、是否在 Select 控制结构中、发送数据所在的地址等; 93 | - 将刚刚创建并初始化的 sudog 结构体加入 sendq 等待队列,并设置到当前 Goroutine 的 waiting 上,表示 Goroutine 正在等待该 sudog 准备就绪; 94 | - 调用 goparkunlock 函数将当前的 Goroutine 更新成 Gwaiting 状态并解锁,该 Goroutine 可以被调用 goready 再次唤醒; 95 | 96 | ``` 97 | func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { 98 | lock(&c.lock) 99 | 100 | if sg := c.recvq.dequeue(); sg != nil { 101 | // 寻找一个等待中的 receiver 102 | // 越过 channel 的 buffer 103 | // 直接把要发的数据拷贝给这个 receiver 104 | // 然后就返 105 | send(c, sg, ep, func() { unlock(&c.lock) }, 3) 106 | return true 107 | } 108 | 109 | // qcount 是 buffer 中已塞进的元素数量 110 | // dataqsize 是 buffer 的总大小 111 | // 说明还有余量 112 | if c.qcount < c.dataqsiz { 113 | // Space is available in the channel buffer. Enqueue the element to send. 114 | qp := chanbuf(c, c.sendx) 115 | 116 | // 将 goroutine 的数据拷贝到 buffer 中 117 | typedmemmove(c.elemtype, qp, ep) 118 | c.sendx++ 119 | 120 | // 环形队列,所以如果已经加到最大了,就回 0 121 | if c.sendx == c.dataqsiz { 122 | c.sendx = 0 123 | } 124 | 125 | // 将 buffer 的元素计数 +1 126 | c.qcount++ 127 | unlock(&c.lock) 128 | return true 129 | } 130 | 131 | // 在 channel 上阻塞,receiver 会帮我们完成后续的工作 132 | gp := getg() 133 | mysg := acquireSudog() 134 | mysg.releasetime = 0 135 | 136 | // 打包 sudog 137 | mysg.elem = ep 138 | mysg.waitlink = nil 139 | mysg.g = gp 140 | mysg.isSelect = false 141 | mysg.c = c 142 | gp.waiting = mysg 143 | gp.param = nil 144 | 145 | // 将当前这个发送 goroutine 打包后的 sudog 入队到 channel 的 sendq 队列中 146 | c.sendq.enqueue(mysg) 147 | 148 | // 将这个发送 g 从 Grunning -> Gwaiting 149 | // 进入休眠 150 | goparkunlock(&c.lock, waitReasonChanSend, traceEvGoBlockSend, 3) 151 | 152 | // 这里是被唤醒后要执行的代码 153 | KeepAlive(ep) 154 | 155 | gp.waiting = nil 156 | gp.param = nil 157 | 158 | mysg.c = nil 159 | releaseSudog(mysg) 160 | return true 161 | } 162 | 163 | func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) { 164 | // receiver 的 sudog 已经在对应区域分配过空间 165 | // 我们只要把数据拷贝过去 166 | if sg.elem != nil { 167 | sendDirect(c.elemtype, sg, ep) 168 | sg.elem = nil 169 | } 170 | gp := sg.g 171 | unlockf() 172 | gp.param = unsafe.Pointer(sg) 173 | 174 | // Gwaiting -> Grunnable 175 | goready(gp, skip+1) 176 | } 177 | 178 | func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) { 179 | // src is on our stack, dst is a slot on another stack. 180 | 181 | // Once we read sg.elem out of sg, it will no longer 182 | // be updated if the destination's stack gets copied (shrunk). 183 | // So make sure that no preemption points can happen between read & use. 184 | dst := sg.elem 185 | typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size) 186 | // No need for cgo write barrier checks because dst is always 187 | // Go memory. 188 | memmove(dst, src, t.size) 189 | } 190 | ``` 191 | 192 | ## receive 接收 193 | 194 | - 直接接收:当 Channel 的 sendq 队列中包含处于等待状态的 Goroutine 时,我们其实就会直接取出队列头的 Goroutine,这里处理的逻辑和发送时所差无几,只是发送数据时调用的是 send 函数,而这里是 recv 函数 195 | - 缓冲区:另一种接收数据时遇到的情况就是,Channel 的缓冲区中已经包含了一些元素,在这时如果使用 <-ch 从 Channel 中接收元素,我们就会直接从缓冲区中 recvx 的索引位置中取出数据进行处理 196 | - 阻塞接收:当 Channel 的 sendq 队列中不存在等待的 Goroutine 并且缓冲区中也不存在任何数据时,从管道中接收数据的操作在大多数时候就会变成一个阻塞的操作. 197 | 198 | ``` 199 | func chanrecv1(c *hchan, elem unsafe.Pointer) { 200 | chanrecv(c, elem, true) 201 | } 202 | 203 | //go:nosplit 204 | func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) { 205 | _, received = chanrecv(c, elem, true) 206 | return 207 | } 208 | 209 | 210 | func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) { 211 | lock(&c.lock) 212 | 213 | // sender 队列中有 sudog 在等待 214 | // 直接从该 sudog 中获取数据拷贝到当前 g 即可 215 | if sg := c.sendq.dequeue(); sg != nil { 216 | recv(c, sg, ep, func() { unlock(&c.lock) }, 3) 217 | return true, true 218 | } 219 | 220 | if c.qcount > 0 { 221 | // Receive directly from queue 222 | qp := chanbuf(c, c.recvx) 223 | 224 | // 直接从 buffer 里拷贝数据 225 | if ep != nil { 226 | typedmemmove(c.elemtype, ep, qp) 227 | } 228 | typedmemclr(c.elemtype, qp) 229 | // 接收索引 +1 230 | c.recvx++ 231 | if c.recvx == c.dataqsiz { 232 | c.recvx = 0 233 | } 234 | // buffer 元素计数 -1 235 | c.qcount-- 236 | unlock(&c.lock) 237 | return true, true 238 | } 239 | 240 | // no sender available: block on this channel. 241 | gp := getg() 242 | mysg := acquireSudog() 243 | mysg.releasetime = 0 244 | if t0 != 0 { 245 | mysg.releasetime = -1 246 | } 247 | // No stack splits between assigning elem and enqueuing mysg 248 | // on gp.waiting where copystack can find it. 249 | // 打包成 sudog 250 | mysg.elem = ep 251 | mysg.waitlink = nil 252 | gp.waiting = mysg 253 | mysg.g = gp 254 | mysg.isSelect = false 255 | mysg.c = c 256 | gp.param = nil 257 | // 进入 recvq 队列 258 | c.recvq.enqueue(mysg) 259 | 260 | // Grunning -> Gwaiting 261 | goparkunlock(&c.lock, "chan receive", traceEvGoBlockRecv, 3) 262 | 263 | // someone woke us up 264 | // 被唤醒 265 | if mysg != gp.waiting { 266 | throw("G waiting list is corrupted") 267 | } 268 | gp.waiting = nil 269 | if mysg.releasetime > 0 { 270 | blockevent(mysg.releasetime-t0, 2) 271 | } 272 | closed := gp.param == nil 273 | gp.param = nil 274 | mysg.c = nil 275 | releaseSudog(mysg) 276 | // 如果 channel 未被关闭,那就是真的 recv 到数据了 277 | return true, !closed 278 | } 279 | 280 | 281 | func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) { 282 | if c.dataqsiz == 0 { 283 | if ep != nil { 284 | // copy data from sender 285 | recvDirect(c.elemtype, sg, ep) 286 | } 287 | } else { 288 | // Queue is full. Take the item at the 289 | // head of the queue. Make the sender enqueue 290 | // its item at the tail of the queue. Since the 291 | // queue is full, those are both the same slot. 292 | qp := chanbuf(c, c.recvx) 293 | 294 | // copy data from queue to receiver 295 | if ep != nil { 296 | typedmemmove(c.elemtype, ep, qp) 297 | } 298 | 299 | // 虽然数据已经给了接受者,但是还是要在 chan 中记录一下 300 | typedmemmove(c.elemtype, qp, sg.elem) 301 | c.recvx++ 302 | if c.recvx == c.dataqsiz { 303 | c.recvx = 0 304 | } 305 | c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz 306 | } 307 | sg.elem = nil 308 | gp := sg.g 309 | unlockf() 310 | gp.param = unsafe.Pointer(sg) 311 | if sg.releasetime != 0 { 312 | sg.releasetime = cputicks() 313 | } 314 | 315 | // Gwaiting -> Grunnable 316 | goready(gp, skip+1) 317 | } 318 | 319 | 320 | ``` 321 | 322 | ## close 关闭 323 | 324 | 在函数执行的最后会为所有被阻塞的 Goroutine 调用 goready 函数重新对这些协程进行调度. 325 | 326 | ``` 327 | func closechan(c *hchan) { 328 | // 上锁,这个锁的粒度比较大,一直到释放完所有的 sudog 才解锁 329 | lock(&c.lock) 330 | 331 | c.closed = 1 332 | 333 | var glist *g 334 | 335 | // release all readers 336 | for { 337 | sg := c.recvq.dequeue() 338 | // 弹出的 sudog 是 nil 339 | // 说明读队列已经空了 340 | if sg == nil { 341 | break 342 | } 343 | 344 | // sg.elem unsafe.Pointer,指向 sudog 的数据元素 345 | // 该元素可能在堆上分配,也可能在栈上 346 | if sg.elem != nil { 347 | // 释放对应的内存 348 | typedmemclr(c.elemtype, sg.elem) 349 | sg.elem = nil 350 | } 351 | if sg.releasetime != 0 { 352 | sg.releasetime = cputicks() 353 | } 354 | 355 | // 将 goroutine 入 glist 356 | // 为最后将全部 goroutine 都 ready 做准备 357 | gp := sg.g 358 | gp.param = nil 359 | gp.schedlink.set(glist) 360 | glist = gp 361 | } 362 | 363 | // release all writers (they will panic) 364 | // 将所有挂在 channel 上的 writer 从 sendq 中弹出 365 | // 该操作会使所有 writer panic 366 | for { 367 | sg := c.sendq.dequeue() 368 | if sg == nil { 369 | break 370 | } 371 | sg.elem = nil 372 | if sg.releasetime != 0 { 373 | sg.releasetime = cputicks() 374 | } 375 | 376 | // 将 goroutine 入 glist 377 | // 为最后将全部 goroutine 都 ready 做准备 378 | gp := sg.g 379 | gp.param = nil 380 | gp.schedlink.set(glist) 381 | glist = gp 382 | } 383 | 384 | // 在释放所有挂在 channel 上的读或写 sudog 时 385 | // 是一直在临界区的 386 | unlock(&c.lock) 387 | 388 | // Ready all Gs now that we've dropped the channel lock. 389 | for glist != nil { 390 | gp := glist 391 | glist = glist.schedlink.ptr() 392 | gp.schedlink = 0 393 | // 使 g 的状态切换到 Grunnable 394 | goready(gp, 3) 395 | } 396 | } 397 | ``` 398 | -------------------------------------------------------------------------------- /Go Defer.md: -------------------------------------------------------------------------------- 1 | # Go Defer 2 | 3 | ## 用法 4 | 5 | ### 常见使用 6 | 7 | 首先要介绍的就是使用 defer 最常见的场景,也就是在 defer 关键字中完成一些收尾的工作,例如在 defer 中回滚一个数据库的事务: 8 | 9 | ``` 10 | func createPost(db *gorm.DB) error { 11 | tx := db.Begin() 12 | defer tx.Rollback() 13 | 14 | if err := tx.Create(&Post{Author: "Draveness"}).Error; err != nil { 15 | return err 16 | } 17 | 18 | return tx.Commit().Error 19 | } 20 | ``` 21 | 22 | 在使用数据库事务时,我们其实可以使用如上所示的代码在创建事务之后就立刻调用 Rollback 保证事务一定会回滚,哪怕事务真的执行成功了,那么在调用 tx.Commit() 之后再执行 tx.Rollback() 其实也不会影响已经提交的事务。 23 | 24 | ### 作用域 25 | 26 | 当我们在一个 for 循环中使用 defer 时也会在退出函数之前执行其中的代码,下面的代码总共调用了五次 defer 关键字: 27 | 28 | ``` 29 | func main() { 30 | for i := 0; i < 5; i++ { 31 | defer fmt.Println(i) 32 | } 33 | } 34 | 35 | $ go run main.go 36 | 4 37 | 3 38 | 2 39 | 1 40 | 0 41 | 42 | ``` 43 | 44 | ### 传值 45 | 46 | Go 语言中所有的函数调用其实都是值传递的,defer 虽然是一个关键字,但是也继承了这个特性,假设我们有以下的代码,在运行这段代码时会打印出 0: 47 | 48 | ``` 49 | type Test struct { 50 | value int 51 | } 52 | 53 | func (t Test) print() { 54 | println(t.value) 55 | } 56 | 57 | func main() { 58 | test := Test{} 59 | defer test.print() 60 | test.value += 1 61 | } 62 | 63 | $ go run main.go 64 | 0 65 | 66 | ``` 67 | 68 | 这其实表明当 defer 调用时其实会对函数中引用的外部参数进行拷贝,所以 test.value += 1 操作并没有修改被 defer 捕获的 test 结构体,不过如果我们修改 print 函数签名的话,其实结果就会稍有不同: 69 | 70 | ``` 71 | type Test struct { 72 | value int 73 | } 74 | 75 | func (t *Test) print() { 76 | println(t.value) 77 | } 78 | 79 | func main() { 80 | test := Test{} 81 | defer test.print() 82 | test.value += 1 83 | } 84 | 85 | $ go run main.go 86 | 1 87 | 88 | ``` 89 | 90 | ### defer 调用时机 91 | 92 | ``` 93 | func f() (result int) { 94 | defer func() { 95 | result++ 96 | }() 97 | 98 | return 0 99 | } 100 | 101 | func f() (r int) { 102 | t := 5 103 | defer func() { 104 | t=t+5 105 | }() 106 | 107 | return t 108 | } 109 | 110 | func f() (r int) { 111 | defer func(r int) { 112 | r=r+5 113 | }(r) 114 | 115 | return 1 116 | } 117 | ``` 118 | 119 | 使用defer时,用一个简单的转换规则改写一下,就不会迷糊了。改写规则是将return语句拆成两句写,return xxx会被改 写成: 120 | 121 | ``` 122 | 返回值 = xxx 123 | 调用defer函数 124 | 空的return 125 | 126 | ``` 127 | 128 | 先看例1,它可以改写成这样: 129 | 130 | ``` 131 | func f() (result int) { 132 | result = 0 //return语句不是一条原子调用,return xxx其实是赋值+ret指令 133 | func() { //defer被插入到return之前执行,也就是赋返回值和ret指令之间 134 | result++ 135 | }() 136 | 137 | return 138 | } 139 | ``` 140 | 141 | 所以这个返回值是1。 142 | 143 | 再看例2,它可以改写成这样: 144 | 145 | ``` 146 | func f() (r int) { 147 | t := 5 148 | r = t 149 | func() { 150 | t=t+5 151 | }() 152 | 153 | return 154 | } 155 | 156 | ``` 157 | 158 | 所以这个的结果是5。 159 | 160 | 最后看例3,它改写后变成: 161 | 162 | ``` 163 | func f() (r int) { 164 | r = 1 165 | func(r int) { 166 | r=r+5 167 | }(r) 168 | 169 | return 1 170 | } 171 | ``` 172 | 173 | 所以这个例子的结果是1。 174 | 175 | ## 数据结构 176 | 177 | ``` 178 | type _defer struct { 179 | siz int32 180 | started bool 181 | sp uintptr 182 | pc uintptr 183 | fn *funcval 184 | _panic *_panic 185 | link *_defer 186 | } 187 | ``` 188 | 189 | 在 _defer 结构中的 sp 和 pc 分别指向了栈指针和调用方的程序计数器,fn 存储的就是向 defer 关键字中传入的函数了。 190 | 191 | ## 原理 192 | 193 | 在 Go 语言的编译期间,编译器不仅将 defer 转换成了 deferproc 的函数调用,还在所有调用 defer 的函数结尾(返回之前)插入了 deferreturn。 194 | 195 | 每一个 defer 关键字都会被转换成 deferproc,在这个函数中我们会为 defer 创建一个新的 _defer 结构体并设置它的 fn、pc 和 sp 参数,除此之外我们会将 defer 相关的函数都拷贝到紧挨着结构体的内存空间中: 196 | 197 | ``` 198 | func deferproc(siz int32, fn *funcval) { 199 | sp := getcallersp() 200 | argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn) 201 | callerpc := getcallerpc() 202 | 203 | d := newdefer(siz) 204 | if d._panic != nil { 205 | throw("deferproc: d.panic != nil after newdefer") 206 | } 207 | d.fn = fn 208 | d.pc = callerpc 209 | d.sp = sp 210 | switch siz { 211 | case 0: 212 | case sys.PtrSize: 213 | *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp)) 214 | default: 215 | memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz)) 216 | } 217 | 218 | return0() 219 | } 220 | 221 | ``` 222 | 223 | 上述函数最终会使用 return0 返回,这个函数的主要作用就是避免在 deferproc 函数中使用 return 返回时又会导致 deferreturn 函数的执行,这也是唯一一个不会触发 defer 的函数了。 224 | 225 | deferproc 中调用的 newdefer 主要作用就是初始化或者取出一个新的 _defer 结构体: 226 | 227 | ``` 228 | func newdefer(siz int32) *_defer { 229 | var d *_defer 230 | sc := deferclass(uintptr(siz)) 231 | gp := getg() 232 | if sc < uintptr(len(p{}.deferpool)) { 233 | pp := gp.m.p.ptr() 234 | if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil { 235 | lock(&sched.deferlock) 236 | for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil { 237 | d := sched.deferpool[sc] 238 | sched.deferpool[sc] = d.link 239 | d.link = nil 240 | pp.deferpool[sc] = append(pp.deferpool[sc], d) 241 | } 242 | unlock(&sched.deferlock) 243 | } 244 | if n := len(pp.deferpool[sc]); n > 0 { 245 | d = pp.deferpool[sc][n-1] 246 | pp.deferpool[sc][n-1] = nil 247 | pp.deferpool[sc] = pp.deferpool[sc][:n-1] 248 | } 249 | } 250 | if d == nil { 251 | total := roundupsize(totaldefersize(uintptr(siz))) 252 | d = (*_defer)(mallocgc(total, deferType, true)) 253 | } 254 | d.siz = siz 255 | d.link = gp._defer 256 | gp._defer = d 257 | return d 258 | } 259 | 260 | ``` 261 | 262 | 从最后的一小段代码我们可以看出,所有的 `_defer` 结构体都会关联到所在的 Goroutine 上并且每创建一个新的 `_defer` 都会追加到协程持有的 `_defer` 链表的最前面。 263 | 264 | deferreturn 其实会从 Goroutine 的链表中取出链表最前面的 _defer 结构体并调用 jmpdefer 函数并传入需要执行的函数和参数: 265 | 266 | ``` 267 | func deferreturn(arg0 uintptr) { 268 | gp := getg() 269 | d := gp._defer 270 | if d == nil { 271 | return 272 | } 273 | sp := getcallersp() 274 | 275 | switch d.siz { 276 | case 0: 277 | case sys.PtrSize: 278 | *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d)) 279 | default: 280 | memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz)) 281 | } 282 | fn := d.fn 283 | d.fn = nil 284 | gp._defer = d.link 285 | freedefer(d) 286 | jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) 287 | } 288 | 289 | ``` 290 | 291 | jmpdefer 其实是一个用汇编语言实现的函数,在不同的处理器架构上的实现稍有不同,但是具体的执行逻辑都差不太多,它们的工作其实就是跳转到并执行 defer 所在的代码段并在执行结束之后跳转回 defereturn 函数。 292 | 293 | ``` 294 | TEXT runtime·jmpdefer(SB), NOSPLIT, $0-8 295 | MOVL fv+0(FP), DX // fn 296 | MOVL argp+4(FP), BX // caller sp 297 | LEAL -4(BX), SP // caller sp after CALL 298 | #ifdef GOBUILDMODE_shared 299 | SUBL $16, (SP) // return to CALL again 300 | #else 301 | SUBL $5, (SP) // return to CALL again 302 | #endif 303 | MOVL 0(DX), BX 304 | JMP BX // but first run the deferred function 305 | 306 | ``` 307 | 308 | -------------------------------------------------------------------------------- /Go Map.md: -------------------------------------------------------------------------------- 1 | # Go Map 2 | 3 | [TOC] 4 | 5 | ## 数据结构 6 | 7 | ### hmap 8 | 9 | ``` 10 | // A header for a Go map. 11 | type hmap struct { 12 | count int // map 中的元素个数,必须放在 struct 的第一个位置,因为 内置的 len 函数会从这里读取 13 | flags uint8 14 | B uint8 // log_2 of # of buckets (最多可以放 loadFactor * 2^B 个元素,再多就要 hashGrow 了) 15 | noverflow uint16 // overflow 的 bucket 的近似数 16 | hash0 uint32 // hash seed 17 | 18 | buckets unsafe.Pointer // 2^B 大小的数组,如果 count == 0 的话,可能是 nil 19 | oldbuckets unsafe.Pointer // 一半大小的之前的 bucket 数组,只有在 growing 过程中是非 nil 20 | nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated) 21 | 22 | extra *mapextra // 当 key 和 value 都可以 inline 的时候,就会用这个字段 23 | } 24 | 25 | type mapextra struct { 26 | // 如果 key 和 value 都不包含指针,并且可以被 inline(<=128 字节) 27 | // 使用 extra 来存储 overflow bucket,这样可以避免 GC 扫描整个 map 28 | // 然而 bmap.overflow 也是个指针。这时候我们只能把这些 overflow 的指针 29 | // 都放在 hmap.extra.overflow 和 hmap.extra.oldoverflow 中了 30 | // overflow 包含的是 hmap.buckets 的 overflow 的 bucket 31 | // oldoverflow 包含扩容时的 hmap.oldbuckets 的 overflow 的 bucket 32 | overflow *[]*bmap 33 | oldoverflow *[]*bmap 34 | 35 | // 指向空闲的 overflow bucket 的指针 36 | nextOverflow *bmap 37 | } 38 | ``` 39 | 40 | - count 用于记录当前哈希表元素数量,这个字段让我们不再需要去遍历整个哈希表来获取长度; 41 | - B 表示了当前哈希表持有的 buckets 数量,但是因为哈希表的扩容是以 2 倍数进行的,所以这里会使用对数来存储,我们可以简单理解成 len(buckets) == 2^B; 42 | - hash0 是哈希的种子,这个值会在调用哈希函数的时候作为参数传进去,它的主要作用就是为哈希函数的结果引入一定的随机性; 43 | - oldbuckets 是哈希在扩容时用于保存之前 buckets 的字段,它的大小都是当前 buckets 的一半; 44 | 45 | ### bmap 46 | 47 | 哈希表的类型其实都存储在每一个桶中,这个桶的结构体 bmap 其实在 Go 语言源代码中的定义只包含一个简单的 tophash 字段: 48 | 49 | ``` 50 | type bmap struct { 51 | tophash [bucketCnt]uint8 52 | } 53 | ``` 54 | 55 | 哈希表中桶的真正结构其实是在编译期间运行的函数 bmap 中被『动态』创建的,我们可以根据上面这个函数的实现对结构体 bmap 进行重建: 56 | 57 | ``` 58 | type bmap struct { 59 | topbits [8]uint8 60 | keys [8]keytype 61 | values [8]valuetype 62 | pad uintptr 63 | overflow uintptr 64 | } 65 | 66 | ``` 67 | 68 | 每一个哈希表中的桶最多只能存储 8 个元素,如果桶中存储的元素超过 8 个,那么这个哈希表的执行效率一定会急剧下降,不过在实际使用中如果一个哈希表存储的数据逐渐增多,我们会对哈希表进行扩容或者使用额外的桶存储溢出的数据,不会让单个桶中的数据超过 8 个。 69 | 70 | ![](img/bucket.png) 71 | 72 | ## 初始化 73 | 74 | 75 | ``` 76 | // make(map[k]v, hint) 77 | // 如果编译器认为 map 和第一个 bucket 可以直接创建在栈上,h 和 bucket 可能都是非空 78 | // h != nil,可以直接在 h 内创建 map 79 | // 如果 h.buckets != nil,其指向的 bucket 可以作为第一个 bucket 来使用 80 | func makemap(t *maptype, hint int, h *hmap) *hmap { 81 | // 初始化 hmap 82 | if h == nil { 83 | h = (*hmap)(newobject(t.hmap)) 84 | } 85 | h.hash0 = fastrand() 86 | 87 | // 按照提供的元素个数,找一个可以放得下这么多元素的 B 值 88 | B := uint8(0) 89 | for overLoadFactor(hint, B) { 90 | B++ 91 | } 92 | h.B = B 93 | 94 | // 分配初始的 hash table 95 | // 如果 B == 0,buckets 字段会由 mapassign 来 lazily 分配 96 | // 因为如果 hint 很大的话,对这部分内存归零会花比较长时间 97 | if h.B != 0 { 98 | var nextOverflow *bmap 99 | h.buckets, nextOverflow = makeBucketArray(t, h.B) 100 | if nextOverflow != nil { 101 | h.extra = new(mapextra) 102 | h.extra.nextOverflow = nextOverflow 103 | } 104 | } 105 | 106 | return h 107 | } 108 | 109 | ``` 110 | 111 | 这个函数会通过 fastrand 创建一个随机的哈希种子,然后根据传入的 hint 计算出需要的最小需要的桶的数量,最后再使用 makeBucketArray创建用于保存桶的数组,这个方法其实就是根据传入的 B 计算出的需要创建的桶数量在内存中分配一片连续的空间用于存储数据,在创建桶的过程中还会额外创建一些用于保存溢出数据的桶,数量是 2^(B-4) 个。 112 | 113 | ![](img/hmap.png) 114 | 115 | ## 元素访问 116 | 117 | 赋值语句左侧接受参数的个数也会影响最终调用的运行时参数,当接受参数仅为一个时,会使用 mapaccess1 函数,同时接受键对应的值以及一个指示键是否存在的布尔值时就会使用 mapaccess2 函数,mapaccess1 函数仅会返回一个指向目标值的指针: 118 | 119 | ``` 120 | func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool) { 121 | // map 为空,或者元素数为 0,直接返回未找到 122 | if h == nil || h.count == 0 { 123 | return unsafe.Pointer(&zeroVal[0]), false 124 | } 125 | if h.flags&hashWriting != 0 { 126 | throw("concurrent map read and map write") 127 | } 128 | alg := t.key.alg 129 | // 不同类型的 key,所用的 hash 算法是不一样的 130 | // 具体可以参考 algarray 131 | hash := alg.hash(key, uintptr(h.hash0)) 132 | // 如果 B = 3,那么结果用二进制表示就是 111 133 | // 如果 B = 4,那么结果用二进制表示就是 1111 134 | m := bucketMask(h.B) 135 | // 按位 &,可以 select 出对应的 bucket 136 | b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + (hash&m)*uintptr(t.bucketsize))) 137 | // 会用到 h.oldbuckets 时,说明 map 发生了扩容 138 | // 这时候,新的 buckets 里可能还没有老的内容 139 | // 所以一定要在老的里面找,否则有可能发生“消失”的诡异现象 140 | if c := h.oldbuckets; c != nil { 141 | if !h.sameSizeGrow() { 142 | // 说明之前只有一半的 bucket,需要除 2 143 | m >>= 1 144 | } 145 | oldb := (*bmap)(unsafe.Pointer(uintptr(c) + (hash&m)*uintptr(t.bucketsize))) 146 | if !evacuated(oldb) { // 如果当前的 bucket 并没有进行数据迁移,那么访问旧的 bucket 147 | b = oldb 148 | } 149 | } 150 | // tophash 取其高 8bit 的值 151 | top := tophash(hash) 152 | for ; b != nil; b = b.overflow(t) { 153 | // 一个 bucket 在存储满 8 个元素后,就再也放不下了 154 | // 这时候会创建新的 bucket 155 | // 挂在原来的 bucket 的 overflow 指针成员上 156 | for i := uintptr(0); i < bucketCnt; i++ { 157 | // 循环对比 bucket 中的 tophash 数组 158 | // 如果找到了相等的 tophash,那说明就是这个 bucket 了 159 | if b.tophash[i] != top { 160 | continue 161 | } 162 | k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) 163 | if t.indirectkey { 164 | k = *((*unsafe.Pointer)(k)) 165 | } 166 | if alg.equal(key, k) { 167 | v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize)) 168 | if t.indirectvalue { 169 | v = *((*unsafe.Pointer)(v)) 170 | } 171 | return v, true 172 | } 173 | } 174 | } 175 | 176 | // 所有 bucket 都没有找到,返回零值和 false 177 | return unsafe.Pointer(&zeroVal[0]), false 178 | } 179 | 180 | ``` 181 | 182 | ## 赋值 183 | 184 | 185 | ``` 186 | // 和 mapaccess 函数差不多,但在没有找到 key 时,会为 key 分配一个新的槽位 187 | func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer { 188 | // 调用对应类型的 hash 算法 189 | alg := t.key.alg 190 | hash := alg.hash(key, uintptr(h.hash0)) 191 | 192 | // 调用 alg.hash 设置 hashWriting 的 flag,因为 alg.hash 可能会 panic 193 | // 这时候我们没法完成一次写操作 194 | h.flags |= hashWriting 195 | 196 | if h.buckets == nil { 197 | // 分配第一个 buckt 198 | h.buckets = newobject(t.bucket) // newarray(t.bucket, 1) 199 | } 200 | 201 | again: 202 | // 计算低 8 位 hash,根据计算出的 bucketMask 选择对应的 bucket 203 | // mask : 1111111 204 | bucket := hash & bucketMask(h.B) 205 | if h.growing() { // 如果正在进行扩容操作 206 | growWork(t, h, bucket) // 对 bucket 进行增量迁移数据 207 | } 208 | // 计算出存储的 bucket 的内存位置 209 | // pos = start + bucketNumber * bucetsize 210 | b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize))) 211 | // 计算高 8 位 hash 212 | top := tophash(hash) 213 | 214 | var inserti *uint8 215 | var insertk unsafe.Pointer 216 | var val unsafe.Pointer 217 | for { 218 | for i := uintptr(0); i < bucketCnt; i++ { 219 | // 遍历 8 个 bucket 中的元素 220 | // 这里的 bucketCnt 是全局常量 221 | if b.tophash[i] != top { 222 | // 在 b.tophash[i] != top 的情况下 223 | // 理论上有可能会是一个空槽位 224 | // 一般情况下 map 的槽位分布是这样的,e 表示 empty: 225 | // [h1][h2][h3][h4][h5][e][e][e] 226 | // 但在执行过 delete 操作时,可能会变成这样: 227 | // [h1][h2][e][e][h5][e][e][e] 228 | // 所以如果再插入的话,会尽量往前面的位置插 229 | // [h1][h2][e][e][h5][e][e][e] 230 | // ^ 231 | // ^ 232 | // 这个位置 233 | // 所以在循环的时候还要顺便把前面的空位置先记下来 234 | if b.tophash[i] == empty && inserti == nil { 235 | // 如果真的在 bucket 里面找不到 key,那么就要在 val 里面插入新值 236 | // 如果这个槽位没有被占,说明可以往这里塞 key 和 value 237 | inserti = &b.tophash[i] // tophash 的插入位置 238 | insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) 239 | val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize)) 240 | } 241 | continue // 由于还没有遍历完毕,继续遍历,查找是否有 key 这个元素 242 | } 243 | 244 | // tophash 相同,key 不一定相同 245 | k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) 246 | if t.indirectkey { 247 | k = *((*unsafe.Pointer)(k)) 248 | } 249 | 250 | // 如果相同的 hash 位置的 key 和要插入的 key 字面上不相等 251 | // 如果两个 key 的首八位后最后八位哈希值一样,就会进行其值比较 252 | // 算是一种哈希碰撞吧 253 | if !alg.equal(key, k) { 254 | continue 255 | } 256 | 257 | // key 也相同,说明找到了元素 258 | // 对应的位置已经有 key 了,直接更新就行 259 | if t.needkeyupdate { 260 | typedmemmove(t.key, k, key) 261 | } 262 | 263 | val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize)) 264 | goto done 265 | } 266 | 267 | // bucket 的 8 个槽没有满足条件的能插入或者能更新的,去 overflow 里继续找 268 | ovf := b.overflow(t) 269 | // 如果 overflow 为 nil,说明到了 overflow 链表的末端了 270 | if ovf == nil { 271 | break 272 | } 273 | // 赋值为链表的下一个元素,继续循环 274 | b = ovf 275 | } 276 | 277 | // 没有找到 key,分配新的空间 278 | 279 | // 如果触发了最大的 load factor,或者已经有太多 overflow buckets 280 | // 并且这个时刻没有在进行 growing 的途中,那么就开始 growing 281 | if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) { 282 | hashGrow(t, h) 283 | // hashGrow 的时候会把当前的 bucket 放到 oldbucket 里 284 | // 但还没有开始分配新的 bucket,所以需要到 again 重试一次 285 | // 重试的时候在 growWork 里会把这个 key 的 bucket 优先分配好 286 | goto again // Growing the table invalidates everything, so try again 287 | } 288 | 289 | if inserti == nil { 290 | // 前面在桶里找的时候,没有找到能塞这个 tophash 的位置 291 | // 说明当前所有 buckets 都是满的,分配一个新的 bucket 292 | newb := h.newoverflow(t, b) 293 | inserti = &newb.tophash[0] 294 | insertk = add(unsafe.Pointer(newb), dataOffset) 295 | val = add(insertk, bucketCnt*uintptr(t.keysize)) 296 | } 297 | 298 | // 没有找到元素,但是找到了可以插入的地方 299 | // 把新的 key 和 value 存储到应插入的位置 300 | if t.indirectkey { 301 | kmem := newobject(t.key) 302 | *(*unsafe.Pointer)(insertk) = kmem 303 | insertk = kmem 304 | } 305 | if t.indirectvalue { 306 | vmem := newobject(t.elem) 307 | *(*unsafe.Pointer)(val) = vmem 308 | } 309 | typedmemmove(t.key, insertk, key) 310 | *inserti = top 311 | h.count++ 312 | 313 | done: 314 | if h.flags&hashWriting == 0 { 315 | throw("concurrent map writes") 316 | } 317 | h.flags &^= hashWriting 318 | if t.indirectvalue { 319 | val = *((*unsafe.Pointer)(val)) 320 | } 321 | return val 322 | } 323 | 324 | ``` 325 | 326 | ## 删除 327 | 328 | 哈希表的删除逻辑与写入逻辑非常相似. 329 | 330 | ``` 331 | func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) { 332 | if h == nil || h.count == 0 { 333 | return 334 | } 335 | if h.flags&hashWriting != 0 { 336 | throw("concurrent map writes") 337 | } 338 | 339 | alg := t.key.alg 340 | hash := alg.hash(key, uintptr(h.hash0)) 341 | 342 | // 调用 alg.hash 设置 hashWriting 的 flag,因为 alg.hash 可能会 panic 343 | // 这时候我们没法完成一次写操作 344 | h.flags |= hashWriting 345 | 346 | // 按低 8 位 hash 值选择 bucket 347 | bucket := hash & bucketMask(h.B) 348 | if h.growing() { 349 | growWork(t, h, bucket) 350 | } 351 | // 按上面算出的桶的索引,找到 bucket 的内存地址 352 | // 并强制转换为需要的 bmap 结构 353 | b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize))) 354 | // 高 8 位 hash 值 355 | top := tophash(hash) 356 | search: 357 | for ; b != nil; b = b.overflow(t) { 358 | for i := uintptr(0); i < bucketCnt; i++ { 359 | // 和上面的差不多,8 个槽位,分别对比 tophash 360 | // 没找到的话就去外围 for 循环的 overflow 链表中继续查找 361 | if b.tophash[i] != top { 362 | continue 363 | } 364 | 365 | // b.tophash[i] == top 366 | // 计算 k 所在的槽位的内存地址 367 | k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) 368 | k2 := k 369 | // 如果 key > 128 字节 370 | if t.indirectkey { 371 | k2 = *((*unsafe.Pointer)(k2)) 372 | } 373 | 374 | // 当高 8 位哈希值相等时,还需要对具体值进行比较 375 | // 以避免哈希冲突时值覆盖 376 | if !alg.equal(key, k2) { 377 | continue 378 | } 379 | 380 | // 如果 key 中是指针,那么清空 key 的内容 381 | if t.indirectkey { 382 | *(*unsafe.Pointer)(k) = nil 383 | } else if t.key.kind&kindNoPointers == 0 { 384 | memclrHasPointers(k, t.key.size) 385 | } 386 | 387 | // 计算 value 所在的内存地址 388 | v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize)) 389 | // 和上面 key 的逻辑差不多 390 | if t.indirectvalue { 391 | *(*unsafe.Pointer)(v) = nil 392 | } else if t.elem.kind&kindNoPointers == 0 { 393 | memclrHasPointers(v, t.elem.size) 394 | } else { 395 | memclrNoHeapPointers(v, t.elem.size) 396 | } 397 | // 设置 tophash[i] = 0 398 | b.tophash[i] = empty 399 | // hmap 的大小计数 -1 400 | h.count-- 401 | break search 402 | } 403 | } 404 | 405 | if h.flags&hashWriting == 0 { 406 | throw("concurrent map writes") 407 | } 408 | h.flags &^= hashWriting 409 | } 410 | 411 | ``` 412 | 413 | ## 扩容 414 | 415 | 扩容触发在 mapassign 中,我们之前注释过了,主要是两点: 416 | 417 | - 是不是已经到了 load factor 的临界点,即元素个数 >= 桶个数 * 6.5,这时候说明大部分的桶可能都快满了,如果插入新元素,有大概率需要挂在 overflow 的桶上。 418 | - overflow 的桶是不是太多了,当 bucket 总数 < 2 ^ 15 时,如果 overflow 的 bucket 总数 >= bucket 的总数,那么我们认为 overflow 的桶太多了。当 bucket 总数 >= 2 ^ 15 时,那我们直接和 2 ^ 15 比较,overflow 的 bucket >= 2 ^ 15 时,即认为溢出桶太多了。为啥会导致这种情况呢?是因为我们对 map 一边插入,一边删除,会导致其中很多桶出现空洞,这样使得 bucket 使用率不高,值存储得比较稀疏。在查找时效率会下降。 419 | 420 | 两种情况官方采用了不同的解决方法: 421 | 422 | - 针对 1,将 B + 1,进而 hmap 的 bucket 数组扩容一倍; 423 | - 针对 2,通过移动 bucket 内容,使其倾向于紧密排列从而提高 bucket 利用率。 424 | 425 | 实际上这里还有一种麻烦的情况,如果 map 中有某个键存在大量的哈希冲突的话,也会导致落入 2 中的判断,这时候对 bucket 的内容进行移动其实没什么意义,反而是纯粹的无用功,所以理论上存在对 Go 的 map 进行 hash 碰撞攻击的可能性。 426 | 427 | 428 | ``` 429 | func hashGrow(t *maptype, h *hmap) { 430 | // 如果已经超过了 load factor 的阈值,那么需要对 map 进行扩容,即 B = B + 1,bucket 总数会变为原来的二倍 431 | // 如果还没到阈值,那么只需要保持相同数量的 bucket,横向拍平就行了 432 | 433 | bigger := uint8(1) 434 | if !overLoadFactor(h.count+1, h.B) { 435 | bigger = 0 436 | h.flags |= sameSizeGrow 437 | } 438 | oldbuckets := h.buckets 439 | newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil) 440 | 441 | flags := h.flags &^ (iterator | oldIterator) 442 | if h.flags&iterator != 0 { 443 | flags |= oldIterator 444 | } 445 | 446 | // 提交扩容结果 447 | h.B += bigger 448 | h.flags = flags 449 | h.oldbuckets = oldbuckets 450 | h.buckets = newbuckets 451 | h.nevacuate = 0 452 | h.noverflow = 0 453 | 454 | if h.extra != nil && h.extra.overflow != nil { 455 | // 把当前的 overflow 赋值给 oldoverflow 456 | if h.extra.oldoverflow != nil { 457 | throw("oldoverflow is not nil") 458 | } 459 | h.extra.oldoverflow = h.extra.overflow 460 | h.extra.overflow = nil 461 | } 462 | if nextOverflow != nil { 463 | if h.extra == nil { 464 | h.extra = new(mapextra) 465 | } 466 | h.extra.nextOverflow = nextOverflow 467 | } 468 | 469 | // 实际的哈希表元素的拷贝工作是在 growWork 和 evacuate 中增量慢慢地进行的 470 | } 471 | ``` 472 | 473 | 在哈希表扩容的过程中,我们会通过 makeBucketArray 创建新的桶数组和一些预创建的溢出桶,随后对将原有的桶数组设置到 oldbuckets 上并将新的空桶设置到 buckets 上,原有的溢出桶也使用了相同的逻辑进行更新。 474 | 475 | ![](img/hashgrow.png) 476 | 477 | 我们在上面的函数中还看不出来 sameSizeGrow 导致的区别,因为这里其实只是创建了新的桶并没有对数据记性任何的拷贝和转移,哈希表真正的『数据迁移』的执行过程其实是在 evacuate 函数中进行的,evacuate 函数会对传入桶中的元素进行『再分配』。 478 | 479 | ``` 480 | func growWork(t *maptype, h *hmap, bucket uintptr) { 481 | // 确保我们移动的 oldbucket 对应的是我们马上就要用到的那一个 482 | evacuate(t, h, bucket&h.oldbucketmask()) 483 | 484 | // 如果还在 growing 状态,再多移动一个 oldbucket 485 | if h.growing() { 486 | evacuate(t, h, h.nevacuate) 487 | } 488 | } 489 | 490 | func evacuate(t *maptype, h *hmap, oldbucket uintptr) { 491 | b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize))) 492 | newbit := h.noldbuckets() 493 | if !evacuated(b) { 494 | // TODO: reuse overflow buckets instead of using new ones, if there 495 | // is no iterator using the old buckets. (If !oldIterator.) 496 | 497 | // xy 包含的是移动的目标 498 | // x 表示新 bucket 数组的前(low)半部分 499 | // y 表示新 bucket 数组的后(high)半部分 500 | var xy [2]evacDst 501 | x := &xy[0] 502 | x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize))) 503 | x.k = add(unsafe.Pointer(x.b), dataOffset) 504 | x.v = add(x.k, bucketCnt*uintptr(t.keysize)) 505 | 506 | if !h.sameSizeGrow() { 507 | // 如果 map 大小(hmap.B)增大了,那么我们只计算 y 508 | // 否则 GC 可能会看到损坏的指针 509 | y := &xy[1] 510 | y.b = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.bucketsize))) 511 | y.k = add(unsafe.Pointer(y.b), dataOffset) 512 | y.v = add(y.k, bucketCnt*uintptr(t.keysize)) 513 | } 514 | 515 | for ; b != nil; b = b.overflow(t) { 516 | k := add(unsafe.Pointer(b), dataOffset) 517 | v := add(k, bucketCnt*uintptr(t.keysize)) 518 | for i := 0; i < bucketCnt; i, k, v = i+1, add(k, uintptr(t.keysize)), add(v, uintptr(t.valuesize)) { 519 | top := b.tophash[i] 520 | if top == empty { 521 | b.tophash[i] = evacuatedEmpty 522 | continue 523 | } 524 | if top < minTopHash { 525 | throw("bad map state") 526 | } 527 | k2 := k 528 | if t.indirectkey { 529 | k2 = *((*unsafe.Pointer)(k2)) 530 | } 531 | var useY uint8 532 | if !h.sameSizeGrow() { 533 | // 计算哈希,以判断我们的数据要转移到哪一部分的 bucket 534 | // 可能是 x 部分,也可能是 y 部分 535 | hash := t.key.alg.hash(k2, uintptr(h.hash0)) 536 | if h.flags&iterator != 0 && !t.reflexivekey && !t.key.alg.equal(k2, k2) { 537 | // 为什么要加 reflexivekey 的判断,可以参考这里: 538 | // https://go-review.googlesource.com/c/go/+/1480 539 | // key != key,只有在 float 数的 NaN 时会出现 540 | // 比如: 541 | // n1 := math.NaN() 542 | // n2 := math.NaN() 543 | // fmt.Println(n1, n2) 544 | // fmt.Println(n1 == n2) 545 | // 这种情况下 n1 和 n2 的哈希值也完全不一样 546 | // 这里官方表示这种情况是不可复现的 547 | // 需要在 iterators 参与的情况下才能复现 548 | // 但是对于这种 key 我们也可以随意对其目标进行发配 549 | // 同时 tophash 对于 NaN 也没啥意义 550 | // 还是按正常的情况下算一个随机的 tophash 551 | // 然后公平地把这些 key 平均分布到各 bucket 就好 552 | useY = top & 1 // 让这个 key 50% 概率去 Y 半区 553 | top = tophash(hash) 554 | } else { 555 | // 这里写的比较 trick 556 | // 比如当前有 8 个桶 557 | // 那么如果 hash & 8 != 0 558 | // 那么说明这个元素的 hash 这种形式 559 | // xxx1xxx 560 | // 而扩容后的 bucketMask 是 561 | // 1111 562 | // 所以实际上这个就是 563 | // xxx1xxx & 1000 > 0 564 | // 说明这个元素在扩容后一定会去上半区 565 | // 所以就是 useY 了 566 | if hash&newbit != 0 { 567 | useY = 1 568 | } 569 | } 570 | } 571 | 572 | if evacuatedX+1 != evacuatedY { 573 | throw("bad evacuatedN") 574 | } 575 | 576 | b.tophash[i] = evacuatedX + useY // evacuatedX + 1 == evacuatedY 577 | dst := &xy[useY] // 移动目标 578 | 579 | if dst.i == bucketCnt { 580 | dst.b = h.newoverflow(t, dst.b) 581 | dst.i = 0 582 | dst.k = add(unsafe.Pointer(dst.b), dataOffset) 583 | dst.v = add(dst.k, bucketCnt*uintptr(t.keysize)) 584 | } 585 | dst.b.tophash[dst.i&(bucketCnt-1)] = top // mask dst.i as an optimization, to avoid a bounds check 586 | if t.indirectkey { 587 | *(*unsafe.Pointer)(dst.k) = k2 // 拷贝指针 588 | } else { 589 | typedmemmove(t.key, dst.k, k) // 拷贝值 590 | } 591 | if t.indirectvalue { 592 | *(*unsafe.Pointer)(dst.v) = *(*unsafe.Pointer)(v) 593 | } else { 594 | typedmemmove(t.elem, dst.v, v) 595 | } 596 | dst.i++ 597 | // These updates might push these pointers past the end of the 598 | // key or value arrays. That's ok, as we have the overflow pointer 599 | // at the end of the bucket to protect against pointing past the 600 | // end of the bucket. 601 | dst.k = add(dst.k, uintptr(t.keysize)) 602 | dst.v = add(dst.v, uintptr(t.valuesize)) 603 | } 604 | } 605 | // Unlink the overflow buckets & clear key/value to help GC. 606 | if h.flags&oldIterator == 0 && t.bucket.kind&kindNoPointers == 0 { 607 | b := add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)) 608 | // Preserve b.tophash because the evacuation 609 | // state is maintained there. 610 | ptr := add(b, dataOffset) 611 | n := uintptr(t.bucketsize) - dataOffset 612 | memclrHasPointers(ptr, n) 613 | } 614 | } 615 | 616 | if oldbucket == h.nevacuate { 617 | advanceEvacuationMark(h, t, newbit) 618 | } 619 | } 620 | 621 | ``` 622 | 623 | evacuate 函数在最开始时会创建一个用于保存分配目的 evacDst 结构体数组,其中保存了目标桶的指针、目标桶存储的元素数量以及当前键和值存储的位置。 624 | 625 | ![](img/hashgrow1.png) 626 | 627 | 如果这是一次不改变大小的扩容,这两个 evacDst 结构体只会初始化一个,当哈希表的容量翻倍时,一个桶中的元素会被分流到新创建的两个桶中,这两个桶同时会被 evacDst 数组引用. 628 | 629 | 如果新的哈希表中有八个桶,在大多数情况下,原来经过桶掩码结果为一的数据会因为桶掩码增加了一位而被分留到了新的一号桶和五号桶,所有的数据也都会被 typedmemmove 拷贝到目标桶的键和值所在的内存空间. 630 | 631 | 该函数的最后会调用 advanceEvacuationMark 函数,它会增加哈希的 nevacuate 计数器,然后在所有的旧桶都被分流后删除这些无用的数据,然而因为 Go 语言数据的迁移过程不是一次性执行完毕的,它只会在写入或者删除时触发 evacuate 函数增量完成的,所以不会瞬间对性能造成影响。 632 | -------------------------------------------------------------------------------- /Go Select.md: -------------------------------------------------------------------------------- 1 | # Go Select 2 | 3 | [TOC] 4 | 5 | ## 数据结构 6 | 7 | select 在 Go 语言的源代码中其实不存在任何的结构体表示,但是 select 控制结构中 case 却使用了 scase 结构体来表示。 8 | 9 | 由于非 default 的 case 中都与 Channel 的发送和接收数据有关,所以在 scase 结构体中也包含一个 c 字段用于存储 case 中使用的 Channel,elem 是用于接收或者发送数据的变量地址、kind 表示当前 case 的种类,总共包含以下四种: 10 | 11 | ``` 12 | const ( 13 | // scase.kind 14 | // send 或者 recv 发生在一个 nil channel 上,就有可能出现这种情况 15 | caseNil = iota 16 | caseRecv 17 | caseSend 18 | caseDefault 19 | ) 20 | 21 | // select 中每一个 case 的数据结构定义 22 | type scase struct { 23 | // 数据元素 24 | elem unsafe.Pointer // data element 25 | // channel 本体 26 | c *hchan // chan 27 | kind uint16 28 | 29 | releasetime int64 30 | pc uintptr // return pc (for race detector / msan) 31 | } 32 | 33 | ``` 34 | 35 | ### 非阻塞的收发 36 | 37 | 如果一个 select 控制结构中包含一个 default 表达式,那么这个 select 并不会等待其它的 Channel 准备就绪,而是会非阻塞地读取或者写入数据: 38 | 39 | ``` 40 | func main() { 41 | ch := make(chan int) 42 | select { 43 | case i := <-ch: 44 | println(i) 45 | 46 | default: 47 | println("default") 48 | } 49 | } 50 | ``` 51 | 52 | 当我们运行上面的代码时其实也并不会阻塞当前的 Goroutine,而是会直接执行 default 条件中的内容并返回。 53 | 54 | ### 随机执行 55 | 56 | 另一个使用 select 遇到的情况其实就是同时有多个 case 就绪后,select 如何进行选择的问题,select 在遇到两个 <-ch 同时响应时其实会随机选择一个 case 执行其中的表达式。 57 | 58 | ## 编译期间 59 | 60 | 编译器在中间代码生成期间会根据 select 中 case 的不同对控制语句进行优化,这一过程其实都发生在 walkselectcases 函数中,我们在这里会分四种情况分别介绍优化的过程和结果: 61 | 62 | - select 中不存在任何的 case; 63 | - select 中只存在一个 case; 64 | - select 中存在两个 case,其中一个 case 是 default 语句; 65 | - 通用的 select 条件; 66 | 67 | ### 不存在任何的 case 68 | 69 | 首先介绍的其实就是最简单的情况,也就是当 select 结构中不包含任何的 case 时,编译器是如何进行处理的: 70 | 71 | ``` 72 | func walkselectcases(cases *Nodes) []*Node { 73 | n := cases.Len() 74 | 75 | if n == 0 { 76 | return []*Node{mkcall("block", nil, nil)} 77 | } 78 | // ... 79 | } 80 | 81 | ``` 82 | 83 | 这段代码非常简单并且容易理解,它直接将类似 select {} 的空语句,转换成对 block 函数的调用: 84 | 85 | ``` 86 | func block() { 87 | gopark(nil, nil, waitReasonSelectNoCases, traceEvGoStop, 1) 88 | } 89 | ``` 90 | 91 | 这其实也在告诉我们一个空的 select 语句会直接阻塞当前的 Goroutine。 92 | 93 | 94 | ### 只包含一个 case 95 | 96 | 如果当前的 select 条件只包含一个 case,那么就会就会执行如下的优化策略将原来的 select 语句改写成 if 条件语句,下面是在 select 中从 Channel 接受数据时被改写的情况: 97 | 98 | ``` 99 | select { 100 | case v, ok <-ch: 101 | // ... 102 | } 103 | 104 | if ch == nil { 105 | block() 106 | } 107 | v, ok := <-ch 108 | // ... 109 | 110 | ``` 111 | 112 | 我们可以看到如果在 select 中仅存在一个 case,那么当 case 中处理的 Channel 是空指针时,就会发生和没有 case 的 select 语句一样的情况,也就是直接挂起当前 Goroutine 并且永远不会被唤醒。 113 | 114 | ### 两个 case,default 语句 115 | 116 | 在下一次的优化策略执行之前,walkselectcases 函数会先将 case 中所有 Channel 都转换成指向 Channel 的地址以便于接下来的优化和通用逻辑的执行,改写之后就会进行最后一次的代码优化,触发的条件就是 — select 中包含两个 case,但是其中一个是 default,我们可以分成发送和接收两种情况介绍处理的过程。 117 | 118 | - 发送 119 | 120 | 首先就是 Channel 的发送过程,也就是 case 中的表达式是 OSEND 类型,在这种情况下会使用 if/else 语句改写代码: 121 | 122 | ``` 123 | select { 124 | case ch <- i: 125 | // ... 126 | default: 127 | // ... 128 | } 129 | 130 | if selectnbsend(ch, i) { 131 | // ... 132 | } else { 133 | // ... 134 | } 135 | 136 | ``` 137 | 138 | 这里最重要的函数其实就是 selectnbsend,它的主要作用就是非阻塞地向 Channel 中发送数据,我们在 Channel 一节曾经提到过发送数据的 chansend 函数包含一个 block 参数,这个参数会决定这一次的发送是不是阻塞的: 139 | 140 | ``` 141 | func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) { 142 | return chansend(c, elem, false, getcallerpc()) 143 | } 144 | ``` 145 | 146 | 在这里我们只需要知道当前的发送过程不是阻塞的,哪怕是没有接收方、缓冲区空间不足导致失败了也会立即返回。 147 | 148 | - 接收 149 | 150 | 由于从 Channel 中接收数据可能会返回一个或者两个值,所以这里的情况会比发送时稍显复杂,不过改写的套路和逻辑确是差不多的: 151 | 152 | ``` 153 | select { 154 | case v <- ch: // case v, received <- ch: 155 | // ... 156 | default: 157 | // ... 158 | } 159 | 160 | if selectnbrecv(&v, ch) { // if selectnbrecv2(&v, &received, ch) { 161 | // ... 162 | } else { 163 | // ... 164 | } 165 | 166 | ``` 167 | 168 | 返回值数量不同会导致最终使用函数的不同,两个用于非阻塞接收消息的函数 selectnbrecv 和 selectnbrecv2 其实只是对 chanrecv 返回值的处理稍有不同: 169 | 170 | ``` 171 | func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) { 172 | selected, _ = chanrecv(c, elem, false) 173 | return 174 | } 175 | 176 | func selectnbrecv2(elem unsafe.Pointer, received *bool, c *hchan) (selected bool) { 177 | selected, *received = chanrecv(c, elem, false) 178 | return 179 | } 180 | 181 | ``` 182 | 183 | - 通用阶段 184 | 185 | 在默认的情况下,select 语句会在编译阶段经过如下过程的处理: 186 | 187 | - 将所有的 case 转换成包含 Channel 以及类型等信息的 scase 结构体; 188 | - 调用运行时函数 selectgo 获取被选择的 scase 结构体索引,如果当前的 scase 是一个接收数据的操作,还会返回一个指示当前 case 是否是接收的布尔值; 189 | - 通过 for 循环生成一组 if 语句,在语句中判断自己是不是被选中的 case 190 | 191 | 一个包含三个 case 的正常 select 语句其实会被展开成如下所示的逻辑,我们可以看到其中处理的三个部分: 192 | 193 | ``` 194 | selv := [3]scase{} 195 | order := [6]uint16 196 | for i, cas := range cases { 197 | c := scase{} 198 | c.kind = ... 199 | c.elem = ... 200 | c.c = ... 201 | } 202 | chosen, revcOK := selectgo(selv, order, 3) 203 | if chosen == 0 { 204 | // ... 205 | break 206 | } 207 | if chosen == 1 { 208 | // ... 209 | break 210 | } 211 | if chosen == 2 { 212 | // ... 213 | break 214 | } 215 | 216 | ``` 217 | 218 | 展开后的 select 其实包含三部分,最开始初始化数组并转换 scase 结构体,使用 selectgo 选择执行的 case 以及最后通过 if 判断选中的情况并执行 case 中的表达式,需要注意的是这里其实也仅仅展开了 select 控制结构,select 语句执行最重要的过程其实也是选择 case 执行的过程,这是我们在下一节运行时重点介绍的。 219 | 220 | ## 运行时 221 | 222 | selectgo 是会在运行期间运行的函数,这个函数的主要作用就是从 select 控制结构中的多个 case 中选择一个需要执行的 case,随后的多个 if 条件语句就会根据 selectgo 的返回值执行相应的语句。 223 | 224 | ### 初始化 225 | 226 | selectgo 函数首先会进行执行必要的一些初始化操作,也就是决定处理 case 的两个顺序,其中一个是 pollOrder 另一个是 lockOrder: 227 | 228 | ``` 229 | func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) { 230 | cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0)) 231 | order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0)) 232 | 233 | scases := cas1[:ncases:ncases] 234 | pollorder := order1[:ncases:ncases] 235 | lockorder := order1[ncases:][:ncases:ncases] 236 | 237 | for i := range scases { 238 | cas := &scases[i] 239 | if cas.c == nil && cas.kind != caseDefault { 240 | *cas = scase{} 241 | } 242 | } 243 | 244 | for i := 1; i < ncases; i++ { 245 | j := fastrandn(uint32(i + 1)) 246 | pollorder[i] = pollorder[j] 247 | pollorder[j] = uint16(i) 248 | } 249 | 250 | // sort the cases by Hchan address to get the locking order. 251 | // ... 252 | 253 | sellock(scases, lockorder) 254 | 255 | // ... 256 | } 257 | 258 | ``` 259 | 260 | Channel 的轮询顺序是通过 fastrandn 随机生成的,这其实就导致了如果多个 Channel 同时『响应』,select 会随机选择其中的一个执行;而另一个 lockOrder 就是根据 Channel 的地址确定的,根据相同的顺序锁定 Channel 能够避免死锁的发生,最后调用的 sellock 就会按照之前生成的顺序锁定所有的 Channel。 261 | 262 | ### 循环 263 | 264 | #### 非阻塞遍历 265 | 266 | 当我们为 select 语句确定了轮询和锁定的顺序并锁定了所有的 Channel 之后就会开始进入 select 的主循环,查找或者等待 Channel 准备就绪,循环中会遍历所有的 case 并找到需要被唤起的 sudog 结构体,在这段循环的代码中,我们会分四种不同的情况处理 select 中的多个 case: 267 | 268 | - caseNil — 当前 case 不包含任何的 Channel,就直接会被跳过; 269 | - caseRecv — 当前 case 会从 Channel 中接收数据; 270 | - 如果当前 Channel 的 sendq 上有等待的 Goroutine 就会直接跳到 recv 标签所在的代码段,从 Goroutine 中获取最新发送的数据; 271 | - 如果当前 Channel 的缓冲区不为空就会跳到 bufrecv 标签处从缓冲区中获取数据; 272 | - 如果当前 Channel 已经被关闭就会跳到 rclose 做一些清除的收尾工作; 273 | - caseSend — 当前 case 会向 Channel 发送数据; 274 | - 如果当前 Channel 已经被关闭就会直接跳到 sclose 代码段; 275 | - 如果当前 Channel 的 recvq 上有等待的 Goroutine 就会跳到 send 代码段向 Channel 直接发送数据; 276 | - caseDefault — 当前 case 表示默认情况,如果循环执行到了这种情况就表示前面的所有 case 都没有被执行,所以这里会直接解锁所有的 Channel 并退出 selectgo 函数,这时也就意味着当前 select 结构中的其他收发语句都是非阻塞的。 277 | 278 | ``` 279 | loop: 280 | // pass 1 - look for something already waiting 281 | var dfli int 282 | var dfl *scase 283 | var casi int 284 | var cas *scase 285 | var recvOK bool 286 | for i := 0; i < ncases; i++ { 287 | casi = int(pollorder[i]) 288 | cas = &scases[casi] 289 | c = cas.c 290 | 291 | switch cas.kind { 292 | case caseNil: 293 | continue 294 | 295 | case caseRecv: 296 | sg = c.sendq.dequeue() 297 | if sg != nil { 298 | goto recv 299 | } 300 | if c.qcount > 0 { 301 | goto bufrecv 302 | } 303 | if c.closed != 0 { 304 | goto rclose 305 | } 306 | 307 | case caseSend: 308 | if raceenabled { 309 | racereadpc(c.raceaddr(), cas.pc, chansendpc) 310 | } 311 | if c.closed != 0 { 312 | goto sclose 313 | } 314 | sg = c.recvq.dequeue() 315 | if sg != nil { 316 | goto send 317 | } 318 | if c.qcount < c.dataqsiz { 319 | goto bufsend 320 | } 321 | 322 | case caseDefault: 323 | dfli = casi 324 | dfl = cas 325 | } 326 | } 327 | ``` 328 | 329 | 这其实是循环执行的第一次遍历,主要作用就是寻找所有 case 中 Channel 是否有可以立刻被处理的情况,无论是在包含等待的 Goroutine 还是缓冲区中存在数据,只要满足条件就会立刻处理。 330 | 331 | 下面是各个 332 | 333 | ``` 334 | bufrecv: 335 | // can receive from buffer 336 | recvOK = true 337 | qp = chanbuf(c, c.recvx) 338 | if cas.elem != nil { 339 | typedmemmove(c.elemtype, cas.elem, qp) 340 | } 341 | typedmemclr(c.elemtype, qp) 342 | c.recvx++ 343 | if c.recvx == c.dataqsiz { 344 | c.recvx = 0 345 | } 346 | c.qcount-- 347 | selunlock(scases, lockorder) 348 | goto retc 349 | 350 | bufsend: 351 | // can send to buffer 352 | typedmemmove(c.elemtype, chanbuf(c, c.sendx), cas.elem) 353 | c.sendx++ 354 | if c.sendx == c.dataqsiz { 355 | c.sendx = 0 356 | } 357 | c.qcount++ 358 | selunlock(scases, lockorder) 359 | goto retc 360 | 361 | recv: 362 | // can receive from sleeping sender (sg) 363 | recv(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2) 364 | if debugSelect { 365 | print("syncrecv: cas0=", cas0, " c=", c, "\n") 366 | } 367 | recvOK = true 368 | goto retc 369 | 370 | rclose: 371 | // read at end of closed channel 372 | selunlock(scases, lockorder) 373 | recvOK = false 374 | if cas.elem != nil { 375 | typedmemclr(c.elemtype, cas.elem) 376 | } 377 | goto retc 378 | 379 | send: 380 | // can send to a sleeping receiver (sg) 381 | send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2) 382 | if debugSelect { 383 | print("syncsend: cas0=", cas0, " c=", c, "\n") 384 | } 385 | goto retc 386 | 387 | sclose: 388 | // send on closed channel 389 | selunlock(scases, lockorder) 390 | panic(plainError("send on closed channel")) 391 | ``` 392 | 393 | #### 加入队列后阻塞 394 | 395 | 如果不能立刻找到活跃的 Channel 就会进入循环的下一个过程,按照需要将当前的 Goroutine 加入到所有 Channel 的 sendq 或者 recvq 队列中: 396 | 397 | ``` 398 | func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) { 399 | // ... 400 | gp = getg() 401 | nextp = &gp.waiting 402 | for _, casei := range lockorder { 403 | casi = int(casei) 404 | cas = &scases[casi] 405 | if cas.kind == caseNil { 406 | continue 407 | } 408 | c = cas.c 409 | sg := acquireSudog() 410 | sg.g = gp 411 | sg.isSelect = true 412 | sg.elem = cas.elem 413 | sg.c = c 414 | *nextp = sg 415 | nextp = &sg.waitlink 416 | 417 | switch cas.kind { 418 | case caseRecv: 419 | c.recvq.enqueue(sg) 420 | 421 | case caseSend: 422 | c.sendq.enqueue(sg) 423 | } 424 | } 425 | 426 | gp.param = nil 427 | gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1) 428 | 429 | // ... 430 | } 431 | ``` 432 | 433 | 这里创建 sudog 并入队的过程其实和 Channel 中直接进行发送和接收时的过程几乎完全相同,只是除了在入队之外,这些 sudog 结构体都会被串成链表附着在当前 Goroutine 上,在入队之后会调用 gopark 函数挂起当前的 Goroutine 等待调度器的唤醒。 434 | 435 | ![](img/waiting.png) 436 | 437 | #### 唤醒 438 | 439 | 等到 select 对应的一些 Channel 准备好之后,当前 Goroutine 就会被调度器唤醒,这时就会继续执行 selectgo 函数中剩下的逻辑,也就是从上面 入队的 sudog 结构体中获取数据: 440 | 441 | ``` 442 | func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) { 443 | // ... 444 | gp.selectDone = 0 445 | sg = (*sudog)(gp.param) 446 | gp.param = nil 447 | 448 | casi = -1 449 | cas = nil 450 | sglist = gp.waiting 451 | gp.waiting = nil 452 | 453 | for _, casei := range lockorder { 454 | k = &scases[casei] 455 | if sg == sglist { 456 | casi = int(casei) 457 | cas = k 458 | } else { 459 | if k.kind == caseSend { 460 | c.sendq.dequeueSudoG(sglist) 461 | } else { 462 | c.recvq.dequeueSudoG(sglist) 463 | } 464 | } 465 | sgnext = sglist.waitlink 466 | sglist.waitlink = nil 467 | releaseSudog(sglist) 468 | sglist = sgnext 469 | } 470 | 471 | c = cas.c 472 | 473 | if cas.kind == caseRecv { 474 | recvOK = true 475 | } 476 | 477 | selunlock(scases, lockorder) 478 | goto retc 479 | // ... 480 | 481 | 482 | retc: 483 | return casi, recvOK 484 | 485 | } 486 | 487 | ``` 488 | 489 | 在第三次根据 lockOrder 遍历全部 case 的过程中,我们会先获取 Goroutine 接收到的参数 param,这个参数其实就是被唤醒的 sudog 结构,我们会依次对比所有 case 对应的 sudog 结构找到被唤醒的 case 并释放其他未被使用的 sudog 结构。 490 | 491 | 由于当前的 select 结构已经挑选了其中的一个 case 进行执行,那么剩下 case 中没有被用到的 sudog 其实就会直接忽略并且释放掉了,为了不影响 Channel 的正常使用,我们还是需要将这些废弃的 sudog 从 Channel 中出队;而除此之外的发生事件导致我们被唤醒的 sudog 结构已经在 Channel 进行收发时就已经出队了,不需要我们再次处理。 492 | 493 | 注意 gp.waiting 就是我们在阻塞睡眠之前,sodug 的链表,存储着所有的 sodug。 -------------------------------------------------------------------------------- /Go Semaphore.md: -------------------------------------------------------------------------------- 1 | # Go Semaphore 2 | 3 | [toc] 4 | 5 | ## 基本概念 6 | 7 | Semaphore 是 Golang 的 mutex 实现的基础,Semaphore semacquire 保证只有 (*addr) 个 Goroutine 获取 Semaphore 成功,其他的 Goroutine 再调用 semacquire 就会直接被调度,等待着其他的 Goroutine 调用 Semrelease 函数复活。 8 | 9 | 有趣的是,当调用 require 的时候,(*addr) 会立刻减一,直到变成 0 之后,就不会再减为负数了。即使有再多的 G 在等待着信号量,`(*addr)` 也是 0。 10 | 11 | 当调用 release 之后,(*addr) 会自动自增,唤醒排在队列里面的 G,G 被唤醒之后,再对 `(*addr)` 递减,所以只要有 G 在 treap 中等待,那么 `(*addr)` 就一直保持着 0,直到所有的 G 都从 treap 中唤醒,这个时候 release 才会使得 `(*addr)` 成为正数,直到还原为初始值。 12 | 13 | 值得注意的是,当 G 被唤醒之后,并不是直接就返回,而是需要再次抢夺 `(*addr)`,因为很可能调用 release 和它刚刚被调度被唤醒中间有一段时间,其他的 G 在这段时间里抢夺了信号量。只有 release 的参数加上 handoff 的时候,release 会对 sodog.ticket 设置为 1,同时直接递减 (*addr) 的值,G 被调度唤醒之后,才能直接获得信号量。 14 | 15 | ``` 16 | //go:linkname sync_runtime_Semacquire sync.runtime_Semacquire 17 | func sync_runtime_Semacquire(addr *uint32) { 18 | semacquire1(addr, false, semaBlockProfile, 0) 19 | } 20 | 21 | //go:linkname poll_runtime_Semacquire internal/poll.runtime_Semacquire 22 | func poll_runtime_Semacquire(addr *uint32) { 23 | semacquire1(addr, false, semaBlockProfile, 0) 24 | } 25 | 26 | //go:linkname sync_runtime_Semrelease sync.runtime_Semrelease 27 | func sync_runtime_Semrelease(addr *uint32, handoff bool, skipframes int) { 28 | semrelease1(addr, handoff, skipframes) 29 | } 30 | 31 | //go:linkname sync_runtime_SemacquireMutex sync.runtime_SemacquireMutex 32 | func sync_runtime_SemacquireMutex(addr *uint32, lifo bool, skipframes int) { 33 | semacquire1(addr, lifo, semaBlockProfile|semaMutexProfile, skipframes) 34 | } 35 | 36 | //go:linkname poll_runtime_Semrelease internal/poll.runtime_Semrelease 37 | func poll_runtime_Semrelease(addr *uint32) { 38 | semrelease(addr) 39 | } 40 | 41 | ``` 42 | 43 | ## 数据结构 44 | 45 | ### semaRoot 46 | 47 | 其实 Semaphore 实现原理很简单: 48 | 49 | - 首先利用 atomic 来判断 `(*addr)` 当前的数值,如果当前 `(*addr)` 还大于 0,那么直接返回继续执行; 50 | 51 | - 如果已经为 0 了,那么就找到 addr 映射的 semaRoot,先自增等待数 semaRoot.nwait。很多个 addr 可能映射到 semtable 数组中同一个 semaRoot。 52 | 53 | - semaRoot 里面有个节点 sudog 类型的 treap 树,这个 treap 树的根就是 semaRoot.treap 变量。 54 | 55 | - 遍历这个 treap 树,这个 treap 树是二叉搜索树,节点是按照 addr 地址大小来排序的。 56 | 57 | - 在这个 treap 树中去找值为 addr 的节点,这个节点是所有被阻塞在 (addr) 上的 Goroutine 组合,把自己入队这个链表。 58 | - gopark 自己当前的 G,等待着唤醒即可。 59 | 60 | 每次调用 semrelease1 的时候,过程相反。 61 | 62 | 我们先来看看数据结构: 63 | 64 | ``` 65 | type semaRoot struct { 66 | lock mutex 67 | treap *sudog // root of balanced tree of unique waiters. 68 | nwait uint32 // Number of waiters. Read w/o the lock. 69 | } 70 | 71 | // Prime to not correlate with any user patterns. 72 | const semTabSize = 251 73 | 74 | var semtable [semTabSize]struct { 75 | root semaRoot 76 | pad [sys.CacheLineSize - unsafe.Sizeof(semaRoot{})]byte 77 | } 78 | 79 | func semroot(addr *uint32) *semaRoot { 80 | return &semtable[(uintptr(unsafe.Pointer(addr))>>3)%semTabSize].root 81 | } 82 | 83 | ``` 84 | 85 | ### sudog 数据结构 86 | 87 | sudog 是 G 结构的包装,用于连接多个等待某个条件的 Goroutine。prev/next 是双向链表的指针。 88 | 89 | 在 treap 中,prev 还代表着其左子树,next 代表着其右子树,parent 是其父节点。waitlink 是阻塞在同一个信号上的队列单链表指针,waittail 是队列尾节点,ticket 是 treap 的随机数,elem 是 treap 的 value(信号量地址),acquiretime 与 releasetime 代表着阻塞的时间。 90 | 91 | ``` 92 | type sudog struct { 93 | g *g 94 | 95 | next *sudog 96 | prev *sudog 97 | elem unsafe.Pointer // data element (may point to stack) 98 | 99 | acquiretime int64 100 | releasetime int64 101 | ticket uint32 102 | parent *sudog // semaRoot binary tree 103 | waitlink *sudog // g.waiting list or semaRoot 104 | waittail *sudog // semaRoot 105 | c *hchan // channel 106 | } 107 | 108 | ``` 109 | 110 | ## semacquire 过程 111 | 112 | 过程如上一个小节所述: 113 | 114 | - 首先利用 cansemacquire 来判断 addr 当前的值,如果还大于 0,那么减一,直接返回当前的 G;如果已经为 0 了,那么就得接着执行 semacquire 函数 115 | - 初始化 sodug 结构体,ticket 是用于 treap 的随机数,保障 treap 大体平衡。acquiretime 是 G 进入调度的时间,对应的 releasetime 被出队的时间。 116 | - root.nwait 自增 117 | - 获取一个锁,这个 mutex 是一个比较底层的实现,是 runtime 专用的一种锁,这个锁实现的临界区通常非常小。如果拿不到当前线程可能会被 futexsleep 一小段时间。 118 | - 拿到锁之后,再次判断当前的 addr 的值,如果已经变成了大于 0,直接解锁,然后返回。 119 | - 在 treap 树中插入节点,入队 120 | - goparkunlock 进行调度,调度开始之前的时候会自动解锁。这样临界区结束。 121 | - 调度回来之后,判断是否真的可以返回,而不是误返回。误返回会重新入队并进入调度。 122 | - s.ticket 代表特殊对待当前的 G,代表着调用 release 函数的时候使用了 handoff 参数,这时候就不需要再利用 cansemacquire 去抢夺信号量了。 123 | - cansemacquire 是其他的 Goroutine 调用了 semrelease 124 | 125 | ``` 126 | func semroot(addr *uint32) *semaRoot { 127 | return &semtable[(uintptr(unsafe.Pointer(addr))>>3)%semTabSize].root 128 | } 129 | 130 | func cansemacquire(addr *uint32) bool { 131 | for { 132 | v := atomic.Load(addr) 133 | if v == 0 { 134 | return false 135 | } 136 | if atomic.Cas(addr, v, v-1) { 137 | return true 138 | } 139 | } 140 | } 141 | 142 | func semacquire(addr *uint32) { 143 | semacquire1(addr, false, 0, 0) 144 | } 145 | 146 | func semacquire1(addr *uint32, lifo bool, profile semaProfileFlags, skipframes int) { 147 | gp := getg() 148 | if gp != gp.m.curg { 149 | throw("semacquire not on the G stack") 150 | } 151 | 152 | // Easy case. 153 | if cansemacquire(addr) { 154 | return 155 | } 156 | 157 | s := acquireSudog() 158 | root := semroot(addr) 159 | t0 := int64(0) 160 | s.releasetime = 0 161 | s.acquiretime = 0 162 | s.ticket = 0 163 | 164 | for { 165 | lock(&root.lock) 166 | // Add ourselves to nwait to disable "easy case" in semrelease. 167 | atomic.Xadd(&root.nwait, 1) 168 | // Check cansemacquire to avoid missed wakeup. 169 | if cansemacquire(addr) { 170 | atomic.Xadd(&root.nwait, -1) 171 | unlock(&root.lock) 172 | break 173 | } 174 | // Any semrelease after the cansemacquire knows we're waiting 175 | // (we set nwait above), so go to sleep. 176 | root.queue(addr, s, lifo) 177 | goparkunlock(&root.lock, waitReasonSemacquire, traceEvGoBlockSync, 4+skipframes) // 开始调度 178 | if s.ticket != 0 || cansemacquire(addr) { // 调度返回 179 | break 180 | } 181 | } 182 | 183 | releaseSudog(s) 184 | } 185 | 186 | ``` 187 | 188 | ## semrelease 过程 189 | 190 | - 对 addr 自增 191 | - 首先判断 root.nwait 是否是 0,如果是的话,就是说明当前整个 root 都没有 G 等待 192 | - 获取锁,然后再次检查 193 | - treap 找到 addr 对应的节点,然后进行出队 194 | - 如果设置了 handoff 参数,那么直接递减 (*addr) 信号量,不需要取出的 G 再进行抢夺。 195 | - readyWithTime 赋值 s.releasetime,然后唤醒对应的 G 196 | 197 | ``` 198 | func semrelease(addr *uint32) { 199 | semrelease1(addr, false, 0) 200 | } 201 | 202 | func semrelease1(addr *uint32, handoff bool, skipframes int) { 203 | root := semroot(addr) 204 | atomic.Xadd(addr, 1) 205 | 206 | if atomic.Load(&root.nwait) == 0 { 207 | return 208 | } 209 | 210 | // Harder case: search for a waiter and wake it. 211 | lock(&root.lock) 212 | if atomic.Load(&root.nwait) == 0 { 213 | // The count is already consumed by another goroutine, 214 | // so no need to wake up another goroutine. 215 | unlock(&root.lock) 216 | return 217 | } 218 | s, t0 := root.dequeue(addr) 219 | if s != nil { 220 | atomic.Xadd(&root.nwait, -1) 221 | } 222 | unlock(&root.lock) 223 | if s != nil { // May be slow, so unlock first 224 | if s.ticket != 0 { 225 | throw("corrupted semaphore ticket") 226 | } 227 | if handoff && cansemacquire(addr) { 228 | s.ticket = 1 229 | } 230 | readyWithTime(s, 5+skipframes) 231 | } 232 | } 233 | 234 | func readyWithTime(s *sudog, traceskip int) { 235 | if s.releasetime != 0 { 236 | s.releasetime = cputicks() 237 | } 238 | goready(s.g, traceskip) 239 | } 240 | ``` 241 | 242 | ## treap queue 入队过程 243 | 244 | sudog 按照地址 hash 到 251 个 bucket 中的其中一个,每一个 bucket 都是一棵 treap。而相同 addr 上的 sudog 会形成一个链表。 245 | 246 | 为啥同一个地址的 sudog 不需要展开放在 treap 中呢?显然,sudog 唤醒的时候,block 在同一个 addr 上的 goroutine,说明都是加的同一把锁,这些 goroutine 被唤醒肯定是一起被唤醒的,相同地址的 g 并不需要查找才能找到,只要决定是先进队列的被唤醒(fifo)还是后进队列的被唤醒(lifo)就可以了。 247 | 248 | ### treap 树插入 249 | 250 | 要理解 treap 树相对于普通二叉搜索树的优点,我们需要先看看普通二叉搜索树的插入过程: 251 | 252 | ``` 253 | int BSTreeNodeInsertR(BSTreeNode **tree,DataType x) //搜索树的插入 254 | { 255 | if(*tree == NULL) 256 | { 257 | *tree = BuyTreeNode(x); 258 | return 0; 259 | } 260 | 261 | if ((*tree)->_data > x) 262 | return BSTreeNodeInsertR(&(*tree)->_left,x); 263 | else if ((*tree)->_data < x) 264 | return BSTreeNodeInsertR(&(*tree)->_right,x); 265 | else 266 | return -1; 267 | } 268 | 269 | ``` 270 | 271 | 我们可以看到,这段代码从根 root 开始递归查找,直到叶子节点,然后把自己挂到叶子节点的左孩子或者右孩子。这种插入方法很容易造成二叉树的不平衡,有的地方深度为 10,有的地方深度为 2。 272 | 273 | AVL 树通过左旋或者右旋,可以实现不破坏二叉树的特征基础上减小局部的深度。但是 AVL 树要求过于严格,它不允许任何左右子树的深度差值超过 1,这样虽然查询特别快速,但是插入操作会进行很多左旋右旋的动作,效率比较低, 274 | 275 | 红黑树采用节点黑、红的特点实现近似的 AVL 树,效率比较高,但是实现上过于繁琐。 276 | 277 | 跳表是另一种实现快速查找的数据结构,采用的是随机建立索引的方式,实现快速查找链表。 278 | 279 | Golang 中大量使用的是 treap 树,这个树在二叉搜索树的基础上,在每个节点上加入随机 ticket。它的原理很简单,对于随机插入的二叉堆,大概率下二叉堆是近似平衡的。 280 | 281 | 我们可以利用这个规则,先找到需要插入的叶子节点,然后赋予一个随机数,利用这个随机数调整最小堆。调整最小堆的过程,并不是普通的直接替换父子节点即可,而是进行左旋与右旋操作,保障二叉树的性质: 282 | 283 | ``` 284 | func (root *semaRoot) queue(addr *uint32, s *sudog, lifo bool) { 285 | s.g = getg() 286 | s.elem = unsafe.Pointer(addr) 287 | s.next = nil 288 | s.prev = nil 289 | 290 | var last *sudog 291 | pt := &root.treap 292 | for t := *pt; t != nil; t = *pt { 293 | last = t 294 | if uintptr(unsafe.Pointer(addr)) < uintptr(t.elem) { 295 | pt = &t.prev // 左子树 296 | } else { 297 | pt = &t.next // 右子树 298 | } 299 | } 300 | 301 | s.ticket = fastrand() | 1 // 随机数的生成 302 | s.parent = last 303 | *pt = s 304 | 305 | // Rotate up into tree according to ticket (priority). 306 | for s.parent != nil && s.parent.ticket > s.ticket { // 最小二叉堆的调整 307 | if s.parent.prev == s { 308 | root.rotateRight(s.parent) // 右旋 309 | } else { 310 | root.rotateLeft(s.parent) // 左旋 311 | } 312 | } 313 | } 314 | 315 | ``` 316 | 317 | 左旋和右旋的代码也比较简单: 318 | 319 | ``` 320 | func (root *semaRoot) rotateLeft(x *sudog) { 321 | // p -> (x a (y b c)) 322 | p := x.parent 323 | a, y := x.prev, x.next 324 | b, c := y.prev, y.next 325 | 326 | y.prev = x 327 | x.parent = y 328 | y.next = c // 个人认为是不必要的步骤 329 | if c != nil { 330 | c.parent = y 331 | } 332 | 333 | x.prev = a 334 | if a != nil { 335 | a.parent = x 336 | } 337 | x.next = b // 个人认为是不必要的步骤 338 | if b != nil { 339 | b.parent = x 340 | } 341 | 342 | y.parent = p 343 | if p == nil { 344 | root.treap = y 345 | } else if p.prev == x { 346 | p.prev = y 347 | } else { 348 | if p.next != x { 349 | throw("semaRoot rotateLeft") 350 | } 351 | p.next = y 352 | } 353 | } 354 | 355 | func (root *semaRoot) rotateRight(y *sudog) { 356 | // p -> (y (x a b) c) 357 | p := y.parent 358 | x, c := y.prev, y.next 359 | a, b := x.prev, x.next 360 | 361 | x.prev = a 362 | if a != nil { 363 | a.parent = x 364 | } 365 | x.next = y 366 | 367 | y.parent = x 368 | y.prev = b 369 | if b != nil { 370 | b.parent = y 371 | } 372 | y.next = c 373 | if c != nil { 374 | c.parent = y 375 | } 376 | 377 | x.parent = p 378 | if p == nil { 379 | root.treap = x 380 | } else if p.prev == y { 381 | p.prev = x 382 | } else { 383 | if p.next != y { 384 | throw("semaRoot rotateRight") 385 | } 386 | p.next = x 387 | } 388 | } 389 | ``` 390 | 391 | ### sodog 链表的插入 392 | 393 | 当 treap 树中已经存在了节点,这个时候就需要更新等待的队列。 394 | 395 | 插入队尾比较简单,只需要更新前一个元素的 waitlink 即可。 396 | 397 | 但是插入队首比较麻烦,因为队首元素要替代之前的队首成为 treap 节点,之前的元素 398 | 399 | ``` 400 | func (root *semaRoot) queue(addr *uint32, s *sudog, lifo bool) { 401 | ... 402 | if t.elem == unsafe.Pointer(addr) { 403 | // Already have addr in list. 404 | if lifo { 405 | // 插入链表队首 406 | 407 | // 继承之前队首的属性 408 | *pt = s 409 | s.ticket = t.ticket 410 | s.acquiretime = t.acquiretime 411 | s.parent = t.parent 412 | s.prev = t.prev 413 | s.next = t.next 414 | 415 | // 更新 treap 节点的左右节点 416 | if s.prev != nil { 417 | s.prev.parent = s 418 | } 419 | if s.next != nil { 420 | s.next.parent = s 421 | } 422 | 423 | // Add t first in s's wait list. 424 | s.waitlink = t 425 | s.waittail = t.waittail 426 | if s.waittail == nil { 427 | s.waittail = t 428 | } 429 | 430 | // 重置之前队首节点的属性 431 | t.parent = nil 432 | t.prev = nil 433 | t.next = nil 434 | t.waittail = nil 435 | } else { 436 | // 插入链表队尾 437 | if t.waittail == nil { 438 | t.waitlink = s 439 | } else { 440 | t.waittail.waitlink = s 441 | } 442 | t.waittail = s 443 | s.waitlink = nil 444 | } 445 | return 446 | } 447 | ... 448 | } 449 | ``` 450 | 451 | ## treap 出队过程 452 | 453 | 454 | ``` 455 | func (root *semaRoot) dequeue(addr *uint32) (found *sudog, now int64) { 456 | ps := &root.treap 457 | s := *ps 458 | for ; s != nil; s = *ps { 459 | if s.elem == unsafe.Pointer(addr) { 460 | goto Found 461 | } 462 | if uintptr(unsafe.Pointer(addr)) < uintptr(s.elem) { 463 | ps = &s.prev 464 | } else { 465 | ps = &s.next 466 | } 467 | } 468 | return nil, 0 469 | 470 | Found: 471 | now = int64(0) 472 | if s.acquiretime != 0 { 473 | now = cputicks() 474 | } 475 | if t := s.waitlink; t != nil { 476 | // 需要用 t 来替代 s 在 treap 的节点 477 | *ps = t 478 | t.ticket = s.ticket 479 | t.parent = s.parent 480 | t.prev = s.prev 481 | 482 | if t.prev != nil { 483 | t.prev.parent = t 484 | } 485 | t.next = s.next 486 | if t.next != nil { 487 | t.next.parent = t 488 | } 489 | 490 | if t.waitlink != nil { 491 | t.waittail = s.waittail 492 | } else { 493 | t.waittail = nil 494 | } 495 | 496 | t.acquiretime = now 497 | s.waitlink = nil 498 | s.waittail = nil 499 | } else { 500 | // 在 treap 中删除 addr 节点 501 | 502 | // 先调整左右子树 503 | // 注意这里是 for 循环,只要 s 还有左右子树就不断的调整 504 | // 左右子树,谁的 ticket 小,谁做父节点 505 | for s.next != nil || s.prev != nil { 506 | if s.next == nil || s.prev != nil && s.prev.ticket < s.next.ticket { 507 | root.rotateRight(s) 508 | } else { 509 | root.rotateLeft(s) 510 | } 511 | } 512 | 513 | // 删除 s 节点,s 现在是叶子节点 514 | if s.parent != nil { 515 | if s.parent.prev == s { 516 | s.parent.prev = nil 517 | } else { 518 | s.parent.next = nil 519 | } 520 | } else { 521 | root.treap = nil 522 | } 523 | } 524 | s.parent = nil 525 | s.elem = nil 526 | s.next = nil 527 | s.prev = nil 528 | s.ticket = 0 529 | return s, now 530 | } 531 | 532 | ```· 533 | 534 | -------------------------------------------------------------------------------- /Go Slice.md: -------------------------------------------------------------------------------- 1 | # Go Slice 2 | 3 | [TOC] 4 | 5 | ## 数组 6 | 7 | 数组是由相同类型元素的集合组成的数据结构,计算机会为数组分配一块连续的内存来保存数组中的元素,我们可以利用数组中元素的索引快速访问元素对应的存储地址,常见的数组大多都是一维的线性数组。 8 | 9 | 数组作为一种数据类型,一般情况下由两部分组成,其中一部分表示了数组中存储的元素类型,另一部分表示数组最大能够存储的元素个数。 10 | 11 | Go 语言中数组的大小在初始化之后就无法改变,数组存储元素的类型相同,但是大小不同的数组类型在 Go 语言看来也是完全不同的,只有两个条件都相同才是同一个类型。 12 | 13 | ``` 14 | func NewArray(elem *Type, bound int64) *Type { 15 | if bound < 0 { 16 | Fatalf("NewArray: invalid bound %v", bound) 17 | } 18 | t := New(TARRAY) 19 | t.Extra = &Array{Elem: elem, Bound: bound} 20 | t.SetNotInHeap(elem.NotInHeap()) 21 | return t 22 | } 23 | 24 | ``` 25 | 26 | 编译期间的数组类型 Array 就包含两个结构,一个是元素类型 Elem,另一个是数组的大小上限 Bound,这两个字段构成了数组类型,而当前数组是否应该在堆栈中初始化也在编译期间就确定了。 27 | 28 | ### 创建 29 | 30 | Go 语言中的数组有两种不同的创建方式,一种是我们显式指定数组的大小,另一种是编译器通过源代码自行推断数组的大小: 31 | 32 | ``` 33 | arr1 := [3]int{1, 2, 3} 34 | arr2 := [...]int{1, 2, 3} 35 | 36 | ``` 37 | 38 | 后一种声明方式在编译期间就会被『转换』成为前一种,下面我们先来介绍数组大小的编译期推导过程。 39 | 40 | 这两种不同的方式会导致编译器做出不同的处理,如果我们使用第一种方式 [10]T,那么变量的类型在编译进行到 类型检查 阶段就会被推断出来,在这时编译器会使用 NewArray 创建包含数组大小的 Array 类型,而如果使用 [...]T 的方式,虽然在这一步也会创建一个 Array 类型 Array{Elem: elem, Bound: -1},但是其中的数组大小上限会是 -1 的结构,这意味着还需要后面的 typecheckcomplit 函数推导该数组的大小: 41 | 42 | ``` 43 | func typecheckcomplit(n *Node) (res *Node) { 44 | // ... 45 | 46 | switch t.Etype { 47 | case TARRAY, TSLICE: 48 | var length, i int64 49 | nl := n.List.Slice() 50 | for i2, l := range nl { 51 | i++ 52 | if i > length { 53 | length = i 54 | } 55 | } 56 | 57 | if t.IsDDDArray() { 58 | t.SetNumElem(length) 59 | } 60 | } 61 | } 62 | 63 | func (t *Type) SetNumElem(n int64) { 64 | t.wantEtype(TARRAY) 65 | at := t.Extra.(*Array) 66 | if at.Bound >= 0 { 67 | Fatalf("SetNumElem array %v already has bound %d", t, at.Bound) 68 | } 69 | at.Bound = n 70 | } 71 | ``` 72 | 73 | 这个删减后的 typecheckcomplit 函数通过遍历元素来推导当前数组的长度,我们能看出 [...]T 类型的声明不是在运行时被推导的,它会在类型检查期间就被推断出正确的数组大小。 74 | 75 | 对于一个由字面量组成的数组,根据数组元素数量的不同,编译器会在负责初始化字面量的 anylit 函数中做两种不同的优化:如果数组中元素的个数小于或者等于 4 个,那么所有的变量会直接在栈上初始化,如果数组元素大于 4 个,变量就会在静态存储区初始化然后拷贝到栈上。 76 | 77 | ### 访问和赋值 78 | 79 | 无论是在栈上还是静态存储区,数组在内存中其实就是一连串的内存空间,表示数组的方法就是一个指向数组开头的指针,这一片内存空间不知道自己存储的是什么变量。 80 | 81 | 数组访问越界的判断也都是在编译期间由静态类型检查完成的。 82 | 83 | 无论是编译器还是字符串,它们的越界错误都会在编译期间发现,但是数组访问操作 OINDEX 会在编译期间被转换成两个 SSA 指令: 84 | 85 | ``` 86 | PtrIndex ptr idx 87 | Load ptr mem 88 | 89 | ``` 90 | 91 | 编译器会先获取数组的内存地址和访问的下标,然后利用 PtrIndex 计算出目标元素的地址,再使用 Load 操作将指针中的元素加载到内存中。 92 | 93 | 数组的赋值和更新操作 a[i] = 2 也会生成 SSA 期间就计算出数组当前元素的内存地址,然后修改当前内存地址的内容,其实会被转换成如下所示的 SSA 操作: 94 | 95 | ``` 96 | LocalAddr {sym} base _ 97 | PtrIndex ptr idx 98 | Store {t} ptr val mem 99 | 100 | ``` 101 | 102 | 在这个过程中会确实能够目标数组的地址,再通过 PtrIndex 获取目标元素的地址,最后将数据存入地址中,从这里我们可以看出无论是数组的寻址还是赋值都是在编译阶段完成的,没有运行时的参与。 103 | 104 | ## 切片 105 | 106 | 在 Golang 中,切片类型的声明与数组有一些相似,由于切片是『动态的』,它的长度并不固定,所以声明类型时只需要指定切片中的元素类型. 107 | 108 | 切片在编译期间的类型应该只会包含切片中的元素类型,NewSlice 就是编译期间用于创建 Slice 类型的函数: 109 | 110 | ``` 111 | func NewSlice(elem *Type) *Type { 112 | if t := elem.Cache.slice; t != nil { 113 | if t.Elem() != elem { 114 | Fatalf("elem mismatch") 115 | } 116 | return t 117 | } 118 | 119 | t := New(TSLICE) 120 | t.Extra = Slice{Elem: elem} 121 | elem.Cache.slice = t 122 | return t 123 | } 124 | 125 | ``` 126 | 127 | 我们可以看到上述方法返回的类型 TSLICE 的 Extra 字段是一个只包含切片内元素类型的 Slice{Elem: elem} 结构,也就是说切片内元素的类型是在编译期间确定的。 128 | 129 | ### 结构 130 | 131 | 编译期间的切片其实就是一个 Slice 类型,但是在运行时切片其实由如下的 SliceHeader 结构体表示,其中 Data 字段是一个指向数组的指针,Len 表示当前切片的长度,而 Cap 表示当前切片的容量,也就是 Data 数组的大小: 132 | 133 | ``` 134 | type SliceHeader struct { 135 | Data uintptr 136 | Len int 137 | Cap int 138 | } 139 | 140 | ``` 141 | 142 | Data 作为一个指针指向的数组其实就是一片连续的内存空间,这片内存空间可以用于存储切片中保存的全部元素,数组其实就是一片连续的内存空间,数组中的元素只是逻辑上的概念,底层存储其实都是连续的,所以我们可以将切片理解成一片连续的内存空间加上长度与容量标识。 143 | 144 | 切片与数组不同,获取数组大小、对数组中的元素的访问和更新在编译期间就已经被转换成了数字和对内存的直接操作,但是切片是运行时才会确定的结构,所有的操作还需要依赖 Go 语言的运行时来完成,我们接下来就会介绍切片的一些常见操作的实现原理。 145 | 146 | ### 初始化 147 | 148 | 首先需要介绍的就是切片的创建过程,Go 语言中的切片总共有两种初始化的方式,一种是使用字面量初始化新的切片,另一种是使用关键字 make 创建切片: 149 | 150 | ``` 151 | slice := []int{1, 2, 3} 152 | slice := make([]int, 10) 153 | 154 | ``` 155 | 156 | 对于字面量 slice,会在 SSA 代码生成阶段被转换成 OpSliceMake 操作 157 | 158 | 对于关键字 make,如果当前的切片不会发生逃逸并且切片非常小的时候,仍然会被转为 OpSliceMake 操作。否则会调用: 159 | 160 | ``` 161 | makeslice(type, len, cap) 162 | 163 | ``` 164 | 165 | 当切片的容量和大小不能使用 int 来表示时,就会实现 makeslice64 处理容量和大小更大的切片,无论是 makeslice 还是 makeslice64,这两个方法都是在结构逃逸到堆上初始化时才需要调用的。 166 | 167 | 接下来,我们回到用于创建切片的 makeslice 函数,这个函数的实现其实非常简单: 168 | 169 | ``` 170 | func makeslice(et *_type, len, cap int) unsafe.Pointer { 171 | mem, overflow := math.MulUintptr(et.size, uintptr(cap)) 172 | if overflow || mem > maxAlloc || len < 0 || len > cap { 173 | mem, overflow := math.MulUintptr(et.size, uintptr(len)) 174 | if overflow || mem > maxAlloc || len < 0 { 175 | panicmakeslicelen() 176 | } 177 | panicmakeslicecap() 178 | } 179 | 180 | return mallocgc(mem, et, true) 181 | } 182 | 183 | ``` 184 | 185 | 上述代码的主要工作就是用切片中元素大小和切片容量相乘计算出切片占用的内存空间,如果内存空间的大小发生了溢出、申请的内存大于最大可分配的内存、传入的长度小于 0 或者长度大于容量,那么就会直接报错,当然大多数的错误都会在编译期间就检查出来,mallocgc 就是用于申请内存的函数,这个函数的实现还是比较复杂,如果遇到了比较小的对象会直接初始化在 Golang 调度器里面的 P 结构中,而大于 32KB 的一些对象会在堆上初始化。 186 | 187 | ### 访问 188 | 189 | 对切片常见的操作就是获取它的长度或者容量,这两个不同的函数 len 和 cap 其实被 Go 语言的编译器看成是两种特殊的操作 OLEN 和 OCAP,它们会在 SSA 生成阶段 被转换成 OpSliceLen 和 OpSliceCap 操作. 190 | 191 | 除了获取切片的长度和容量之外,访问切片中元素使用的 OINDEX 操作也都在 SSA 中间代码生成期间就转换成对地址的获取操作. 192 | 193 | ### 追加 194 | 195 | 向切片中追加元素应该是最常见的切片操作,在 Go 语言中我们会使用 append 关键字向切片中追加元素,追加元素会根据是否 inplace 在中间代码生成阶段转换成以下的两种不同流程,如果 append 之后的切片不需要赋值回原有的变量,也就是如 append(slice, 1, 2, 3) 所示的表达式会被转换成如下的过程: 196 | 197 | ``` 198 | ptr, len, cap := slice 199 | newlen := len + 3 200 | if newlen > cap { 201 | ptr, len, cap = growslice(slice, newlen) 202 | newlen = len + 3 203 | } 204 | *(ptr+len) = 1 205 | *(ptr+len+1) = 2 206 | *(ptr+len+2) = 3 207 | return makeslice(ptr, newlen, cap) 208 | 209 | ``` 210 | 211 | 我们会先对切片结构体进行解构获取它的数组指针、大小和容量,如果新的切片大小大于容量,那么就会使用 growslice 对切片进行扩容并将新的元素依次加入切片并创建新的切片,但是 slice = apennd(slice, 1, 2, 3) 这种 inplace 的表达式就只会改变原来的 slice 变量: 212 | 213 | ``` 214 | a := &slice 215 | ptr, len, cap := slice 216 | newlen := len + 3 217 | if uint(newlen) > uint(cap) { 218 | newptr, len, newcap = growslice(slice, newlen) 219 | vardef(a) 220 | *a.cap = newcap 221 | *a.ptr = newptr 222 | } 223 | newlen = len + 3 224 | *a.len = newlen 225 | *(ptr+len) = 1 226 | *(ptr+len+1) = 2 227 | *(ptr+len+2) = 3 228 | 229 | ``` 230 | 231 | 上述两段代码的逻辑其实差不多,最大的区别在于最后的结果是不是赋值会原有的变量,不过从 inplace 的代码可以看出 Go 语言对类似的过程进行了优化,所以我们并不需要担心 append 会在数组容量足够时导致发生切片的复制。 232 | 233 | 到这里我们已经了解了在切片容量足够时如何向切片中追加元素,但是如果切片的容量不足时就会调用 growslice 为切片扩容: 234 | 235 | ``` 236 | func growslice(et *_type, old slice, cap int) slice { 237 | newcap := old.cap 238 | doublecap := newcap + newcap 239 | if cap > doublecap { 240 | newcap = cap 241 | } else { 242 | if old.len < 1024 { 243 | newcap = doublecap 244 | } else { 245 | for 0 < newcap && newcap < cap { 246 | newcap += newcap / 4 247 | } 248 | if newcap <= 0 { 249 | newcap = cap 250 | } 251 | } 252 | } 253 | 254 | ``` 255 | 256 | 扩容其实就是需要为切片分配一块新的内存空间,分配内存空间之前需要先确定新的切片容量,Go 语言根据切片的当前容量选择不同的策略进行扩容: 257 | 258 | - 如果期望容量大于当前容量的两倍就会使用期望容量; 259 | - 如果当前切片容量小于 1024 就会将容量翻倍; 260 | - 如果当前切片容量大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量; 261 | 262 | 确定了切片的容量之后,我们就可以开始计算切片中新数组的内存占用了,计算的方法就是将目标容量和元素大小相乘: 263 | 264 | ``` 265 | var overflow bool 266 | var lenmem, newlenmem, capmem uintptr 267 | switch { 268 | // ... 269 | default: 270 | lenmem = uintptr(old.len) * et.size 271 | newlenmem = uintptr(cap) * et.size 272 | capmem, overflow = math.MulUintptr(et.size, uintptr(newcap)) 273 | capmem = roundupsize(capmem) 274 | newcap = int(capmem / et.size) 275 | } 276 | 277 | var p unsafe.Pointer 278 | if et.kind&kindNoPointers != 0 { 279 | p = mallocgc(capmem, nil, false) 280 | memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem) 281 | } else { 282 | p = mallocgc(capmem, et, true) 283 | if writeBarrier.enabled { 284 | bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem) 285 | } 286 | } 287 | memmove(p, old.array, lenmem) 288 | 289 | return slice{p, old.len, newcap} 290 | } 291 | 292 | ``` 293 | 294 | 如果当前切片中元素不是指针类型,那么就会调用 memclrNoHeapPointers 函数将超出当前长度的位置置空并在最后使用 memmove 将原数组内存中的内容拷贝到新申请的内存中, 不过无论是 memclrNoHeapPointers 还是 memmove 函数都使用目标机器上的汇编指令进行实现. 295 | 296 | ### 拷贝 297 | 298 | ``` 299 | func slicecopy(to, fm slice, width uintptr) int { 300 | if fm.len == 0 || to.len == 0 { 301 | return 0 302 | } 303 | 304 | n := fm.len 305 | if to.len < n { 306 | n = to.len 307 | } 308 | 309 | if width == 0 { 310 | return n 311 | } 312 | 313 | // ... 314 | 315 | size := uintptr(n) * width 316 | if size == 1 { 317 | *(*byte)(to.array) = *(*byte)(fm.array) 318 | } else { 319 | memmove(to.array, fm.array, size) 320 | } 321 | return n 322 | } 323 | 324 | ``` 325 | 326 | 上述函数的实现非常直接,它将切片中的全部元素通过 memmove 或者数组指针的方式将整块内存中的内容拷贝到目标的内存区域. -------------------------------------------------------------------------------- /Go Sync.md: -------------------------------------------------------------------------------- 1 | # Go Sync 2 | 3 | [TOC] 4 | 5 | ## notifyList 6 | 7 | ### 基本原理 8 | 9 | 相比 Semaphore 来说,sync.Cond 非常简单,它没有 treap 树,只有一个单链表连接着所有被 cond 阻塞的 G。 10 | 11 | notifyList 有两个非常重要的成员,wait 和 notify,这两个都是 int 类型的变量,每次调用 cond.wait,wait 就自增 1,每次调用 cond.notify,notify 就自增 1,并且唤醒 sodog.ticket 和当前 notify 数值相同的 Goroutine。 12 | 13 | ### 数据结构 14 | 15 | ``` 16 | type notifyList struct { 17 | wait uint32 18 | 19 | notify uint32 20 | 21 | // List of parked waiters. 22 | lock mutex 23 | head *sudog 24 | tail *sudog 25 | } 26 | 27 | ``` 28 | 29 | ### notifyListAdd 30 | 31 | 调用 wait 函数之前,要先调用 add 函数,自增 wait 属性。 32 | 33 | ``` 34 | func notifyListAdd(l *notifyList) uint32 { 35 | return atomic.Xadd(&l.wait, 1) - 1 36 | } 37 | 38 | ``` 39 | 40 | ### notifyListWait 41 | 42 | - 如果自增之后的 wait 还是小于等于 notify,那么说明其他 Goroutine 已经调用了 notify 函数,直接返回即可 43 | - 设置 ticket 为当前的 wait 数 44 | - 更新队列链表 45 | - goparkunlock 进行调度 46 | 47 | ``` 48 | func notifyListWait(l *notifyList, t uint32) { 49 | lock(&l.lock) 50 | 51 | // Return right away if this ticket has already been notified. 52 | if less(t, l.notify) { 53 | unlock(&l.lock) 54 | return 55 | } 56 | 57 | // Enqueue itself. 58 | s := acquireSudog() 59 | s.g = getg() 60 | s.ticket = t 61 | s.releasetime = 0 62 | t0 := int64(0) 63 | 64 | if l.tail == nil { 65 | l.head = s 66 | } else { 67 | l.tail.next = s 68 | } 69 | l.tail = s 70 | goparkunlock(&l.lock, waitReasonSyncCondWait, traceEvGoBlockCond, 3) 71 | 72 | releaseSudog(s) 73 | } 74 | 75 | ``` 76 | 77 | ### notifyListNotifyOne 78 | 79 | - 这个函数先判断是否有新增的 waiter,如果没有那也不必 notify 了,直接返回 80 | - 加锁 81 | - 递增 notify 属性值 82 | - 在链表中找到 ticket 为 wait 的那个 sodog,唤醒它 83 | 84 | ``` 85 | func notifyListNotifyOne(l *notifyList) { 86 | // Fast-path: if there are no new waiters since the last notification 87 | // we don't need to acquire the lock at all. 88 | if atomic.Load(&l.wait) == atomic.Load(&l.notify) { 89 | return 90 | } 91 | 92 | lock(&l.lock) 93 | 94 | // Re-check under the lock if we need to do anything. 95 | t := l.notify 96 | if t == atomic.Load(&l.wait) { 97 | unlock(&l.lock) 98 | return 99 | } 100 | 101 | // Update the next notify ticket number. 102 | atomic.Store(&l.notify, t+1) 103 | 104 | for p, s := (*sudog)(nil), l.head; s != nil; p, s = s, s.next { 105 | if s.ticket == t { 106 | n := s.next 107 | if p != nil { 108 | p.next = n 109 | } else { 110 | l.head = n 111 | } 112 | if n == nil { 113 | l.tail = p 114 | } 115 | unlock(&l.lock) 116 | s.next = nil 117 | readyWithTime(s, 4) 118 | return 119 | } 120 | } 121 | unlock(&l.lock) 122 | } 123 | 124 | ``` 125 | 126 | ### notifyListNotifyAll 127 | 128 | ``` 129 | func notifyListNotifyAll(l *notifyList) { 130 | // Fast-path: if there are no new waiters since the last notification 131 | // we don't need to acquire the lock. 132 | if atomic.Load(&l.wait) == atomic.Load(&l.notify) { 133 | return 134 | } 135 | 136 | lock(&l.lock) 137 | s := l.head 138 | l.head = nil 139 | l.tail = nil 140 | 141 | atomic.Store(&l.notify, atomic.Load(&l.wait)) 142 | unlock(&l.lock) 143 | 144 | // Go through the local list and ready all waiters. 145 | for s != nil { 146 | next := s.next 147 | s.next = nil 148 | readyWithTime(s, 4) 149 | s = next 150 | } 151 | } 152 | 153 | ``` 154 | 155 | ## sync.Cond 156 | 157 | ``` 158 | // Cond 实现了一种条件变量,可以让 goroutine 都等待、或宣布一个事件的发生 159 | // 160 | // 每一个 Cond 都有一个对应的 Locker L,可以是一个 *Mutex 或者 *RWMutex 161 | // 当条件发生变化及调用 Wait 方法时,必须持有该锁 162 | // 163 | // Cond 在首次使用之后同样不能被拷贝 164 | type Cond struct { 165 | noCopy noCopy 166 | 167 | // 在观测或修改条件时,必须持有 L 168 | L Locker 169 | 170 | notify notifyList 171 | checker copyChecker 172 | } 173 | 174 | func NewCond(l Locker) *Cond { 175 | return &Cond{L: l} 176 | } 177 | 178 | // Wait 会原子地解锁 c.L,并挂起当前调用 Wait 的 goroutine 179 | // 之后恢复执行时,Wait 在返回之前对 c.L 加锁。和其它系统不一样 180 | // Wait 在被 Broadcast 或 Signal 唤醒之前,是不能返回的 181 | // 182 | // 因为 c.L 在 Wait 第一次恢复执行之后是没有被锁住的,调用方 183 | // 在 Wait 返回之后没办法假定 condition 为 true。 184 | // 因此,调用方应该在循环中调用 Wait 185 | // 186 | // c.L.Lock() 187 | // for !condition() { 188 | // c.Wait() 189 | // } 190 | // .. 这时候 condition 一定为 true.. 191 | // c.L.Unlock() 192 | // 193 | func (c *Cond) Wait() { 194 | c.checker.check() 195 | t := runtime_notifyListAdd(&c.notify) 196 | c.L.Unlock() 197 | runtime_notifyListWait(&c.notify, t) 198 | c.L.Lock() 199 | } 200 | 201 | // Signal 只唤醒等待在 c 上的一个 goroutine。 202 | // 对于 caller 来说在调用 Signal 时持有 c.L 也是允许的,不过没有必要 203 | func (c *Cond) Signal() { 204 | c.checker.check() 205 | runtime_notifyListNotifyOne(&c.notify) 206 | } 207 | 208 | // Broadcast 唤醒所有在 c 上等待的 goroutine 209 | // 同样在调用 Broadcast 时,可以持有 c.L,但没必要 210 | func (c *Cond) Broadcast() { 211 | c.checker.check() 212 | runtime_notifyListNotifyAll(&c.notify) 213 | } 214 | 215 | // 检查结构体是否被拷贝过,因为其持有指向自身的指针 216 | // 指针值和实际地址不一致时,即说明发生了拷贝 217 | type copyChecker uintptr 218 | 219 | func (c *copyChecker) check() { 220 | if uintptr(*c) != uintptr(unsafe.Pointer(c)) && 221 | !atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) && 222 | uintptr(*c) != uintptr(unsafe.Pointer(c)) { 223 | panic("sync.Cond is copied") 224 | } 225 | } 226 | 227 | // noCopy may be embedded into structs which must not be copied 228 | // after the first use. 229 | // 230 | // See https://golang.org/issues/8005#issuecomment-190753527 231 | // for details. 232 | type noCopy struct{} 233 | 234 | // Lock is a no-op used by -copylocks checker from `go vet`. 235 | func (*noCopy) Lock() {} 236 | 237 | ``` 238 | 239 | 240 | ## sync.map 241 | 242 | ### 用法 243 | 244 | - Store 写入 245 | - Load 读取,返回值有两个,第一个是value,第二个是bool变量表示key是否存在 246 | - Delete 删除 247 | - LoadOrStore 存在就读,不存在就写 248 | - Range 遍历,注意遍历的快照 249 | 250 | 并发hashmap的方案有很多,下面简单提一下几种,然后再讨论golang实现时的考虑。 251 | 第一种是最简单的,直接在不支持并发的hashmap上,使用一个读写锁的保护,这也是golang sync map还没出来前,大家常用的方法。这种方法的缺点是写会堵塞读。 252 | 253 | 第二种是数据库常用的方法,分段锁,每一个读写锁保护一段区间,golang的第三方库也有人是这么实现的。java的ConcurrentHashMap也是这么实现的。平均情况下这样的性能还挺好的,但是极端情况下,如果某个区间有热点写,那么那个区间的读请求也会受到影响。 254 | 255 | 第三种方法是我们C++自己造轮子时经常用的,使用使用链表法解决冲突,然后链表使用CAS去解决并发下冲突,这样读写都是无锁,我觉得这种挺好的,性能非常高,不知为啥其他语言不这么实现。 256 | 257 | 然后在《An overview of sync.Map》中有提到,在cpu核数很多的情况下,因为cache contention,reflect.New、sync.RWMutex、atomic.AddUint32都会很慢,golang团队为了适应cpu核很多的情况,没有采用上面的几种常见的方案。 258 | 259 | golang sync map的目标是实现适合读多写少的场景、并且要求稳定性很好,不能出现像分段锁那样读经常被阻塞的情况。golang sync map基于map做了一层封装,在大部分情况下,不过写入性能比较差。下面来详细说说实现。 260 | 261 | ### 基本原理 262 | 263 | 要读受到的影响尽量小,那么最容易想到的想法,就是读写分离。golang sync map也是受到这个想法的启发(我自认为)设计出来的。使用了两个map,一个叫read,一个叫dirty,两个map存储的都是指针,指向value数据本身,所以两个map是共享value数据的,更新value对两个map同时可见。 264 | 265 | #### 增 266 | 267 | 和普通的读写分离不同,并发 map 的 read 实际上除了读,还可以改和删,但是不能进行增。 268 | 269 | read不能新增key,那么数据怎么来的呢?sync map中会记录miss cache的次数,当miss次数大于等于dirty元素个数时,就会把dirty变成read,原来的dirty清空。 270 | 271 | 为了方便dirty直接变成read,那么得保证read中存在的数据dirty必须有,所以在dirty是空的时候,如果要新增一个key,那么会把read中的元素复制到dirty中,然后写入新key。 272 | 273 | 我们可以把这个时间段称之为一个 map 周期。 274 | 275 | #### 改查 276 | 277 | 对于 read 和 dirty 中都存在的 key,直接利用 CAS 对 read 进行删改查就可以了,由于 read 和 dirty 共享 entry,因此 read 一旦更改了 entry,dirty 那里的数据就被一并更改了。 278 | 279 | #### 删 280 | 281 | golang 的并发 map 并不是真正的删除 map 中的 key,而是在 read 中为 entry.p 赋值为 nil,这样 read 和 dirty 这个 key 就会共享 nil,代表数据已经被删除了。这么做,实际上是为删除的 key 做一个缓存,缓存时间为一个 map 周期,假如删除一段时间后,又想重新用这个 key,直接执行更新操作即可,对 map 的影响比较小。 282 | 283 | 前面说过,当 miss 的数量过多,会触发 read 被 dirty 替换,dirty 会被清空。这个时候,被删除的 key 会跟随着 dirty 来到 read,但是他的 value 仍然是 nil。 284 | 285 | 当又有新的 key 需要写入的时候,需要将 read 的所有 key 同步到 dirty 中去。那么 value 是 nil 的那些 key 如何处理呢? 286 | 287 | 答案是,不会被同步到 dirty 中去,但是会把 read 中 value 从 nil 改为 expunged。这个时候被删除的 key 就危险了。 288 | 289 | 如果这个时候,key 被重利用,那么 read 会重新从 expunged 改为 nil,并且向 dirty 写入这个新的 key,它就像一个正常的 key 一样了。 290 | 291 | 但是如果这个 key 一直没有人重新赋值了,那么下一个周期,read 被 dirty 替换的时候,这个 key 就真的不存在了。 292 | 293 | 总结下就是: 294 | 295 | - 周期一:key 被删除,value 在 read 和 dirty 中都是 nil 296 | - 周期二:key 只存在与 read,value 为 expunged;dirty 中没有这个 key 297 | - 周期三:key 在 read 和 dirty 中都不存在。 298 | 299 | ### 数据结构 300 | 301 | - Map 中的 mu 是保护 dirty 的锁 302 | - read 实际上是 readOnly 类型,只是 golang 使用了 atomic 对它进行了保护,让它可以在更新之后,立刻被其他的 Goroutine 看到 303 | - dirty 就是专门负责添加新元素的 map,注意 entry 是指针,它实际上和 readOnly 的 map 共享一个 entry 304 | - misses 就是查询 read 失败的次数,达到一个阈值,就会将 dirty 替换 read 305 | 306 | 注意对于 entry.p,有两个特殊值,一个是nil,另一个是expunged。我们在上面已经讲过了它的作用,分别代表着被删除的 key 所处的周期状态。 307 | 308 | ``` 309 | type Map struct { 310 | mu Mutex 311 | 312 | read atomic.Value // readOnly 313 | 314 | dirty map[interface{}]*entry 315 | 316 | misses int 317 | } 318 | 319 | // readOnly is an immutable struct stored atomically in the Map.read field. 320 | type readOnly struct { 321 | m map[interface{}]*entry 322 | amended bool // true if the dirty map contains some key not in m. 323 | } 324 | 325 | // expunged is an arbitrary pointer that marks entries which have been deleted 326 | // from the dirty map. 327 | var expunged = unsafe.Pointer(new(interface{})) 328 | 329 | // An entry is a slot in the map corresponding to a particular key. 330 | type entry struct { 331 | p unsafe.Pointer // *interface{} 332 | } 333 | 334 | ``` 335 | 336 | ![](img/map.png) 337 | 338 | ### Load 读取 339 | 340 | - 读取时,先去read读取; 341 | - 如果没有,并且 read.amended 为 false,那么说明,自从 read 被 dirty 替换,还没有新的 key 写入,此时 ditry 为空,read 没有那就是真的没有,直接返回 nil,false 342 | - 如果没有,而且 read.amended 为 true,说明有新的 key 写入到了 dirty。那么加锁,然后去 dirty 读取,同时调用 missLocked(),再解锁。 343 | - 在 missLocked 中,会递增 misses 变量,如果 misses>len(dirty),那么把 dirty 提升为 read,清空原来的dirty。 344 | 345 | 在代码中,我们可以看到一个double check,检查read没有,上锁,再检查read中有没有,是因为有可能在第一次检查之后,上锁之前的间隙,dirty提升为read了,这时如果不double check,可能会导致一个存在的key却返回给调用方说不存在。 在下面的其他操作中,我们经常会看到这个double check。 346 | 347 | ``` 348 | func (m *Map) Load(key interface{}) (value interface{}, ok bool) { 349 | read, _ := m.read.Load().(readOnly) 350 | e, ok := read.m[key] 351 | 352 | if !ok && read.amended { 353 | m.mu.Lock() 354 | 355 | read, _ = m.read.Load().(readOnly) 356 | e, ok = read.m[key] 357 | if !ok && read.amended { 358 | e, ok = m.dirty[key] 359 | 360 | m.missLocked() 361 | } 362 | m.mu.Unlock() 363 | } 364 | 365 | if !ok { 366 | return nil, false 367 | } 368 | 369 | return e.load() 370 | } 371 | 372 | func (e *entry) load() (value interface{}, ok bool) { 373 | p := atomic.LoadPointer(&e.p) 374 | if p == nil || p == expunged { 375 | return nil, false 376 | } 377 | return *(*interface{})(p), true 378 | } 379 | 380 | func (m *Map) missLocked() { 381 | m.misses++ 382 | if m.misses < len(m.dirty) { 383 | return 384 | } 385 | m.read.Store(readOnly{m: m.dirty}) 386 | m.dirty = nil 387 | m.misses = 0 388 | } 389 | ``` 390 | 391 | ### Store 写入 392 | 393 | - 写入的时候,先看read中能否查到key, 394 | - 在read中存在的话,而且不是 expunged 状态,直接通过read中的entry来更新值;但是如果更新的过程中,被别的 G 更改为 expunged,那就不能再更新了,因为会涉及到 dirty 的写入。 395 | - 如果是 expunged 状态,因为涉及到 dirty 的写入,所以要加锁。 396 | - 在read中不存在,那么就上锁,然后double check。这里需要留意,分几种情况: 397 | - double check发现read中存在,直接更新。 398 | - double check发现read中存在,但是是expunged,那么有可能加锁之前就是 expunged 状态,或者在加锁的过程中,这个 key 经历了周期一的删除,现在处于周期二的 read 中。我们需要将 expunged 改为 nil,并且将 key 添加到 dirty 中,然后更新它的值。 399 | - dirty中存在,直接更新。说明这个 key 是当前这个周期新建的 key,只存在与 dirty 中,还没有同步到 read 中去。 400 | - read 和 dirty中都不存在,那就是说明此时不是更新操作,而是插入的操作: 401 | - 如果 read.amended 为 false,那这次就是自从 read 被 dirty 替换之后的第一次新 key 插入。需要将read复制到dirty中,最后再把新值写入到dirty中。复制的时候调用的是dirtyLocked(),在复制到dirty的时候,read中为nil的元素,会更新为expunged,并且不复制到dirty中。 402 | - 如果 read.amended 为 true,那么直接对 dirty 插入新增就可以了 403 | 404 | 我们可以看到,在更新read中的数据时,使用的是tryStore,通过CAS来解决冲突,在CAS出现冲突后,如果发现数据被置为expung,tryStore那么就不会写入数据,而是会返回false,在Store流程中,就是接着往下走,在dirty中写入。 405 | 406 | 407 | ``` 408 | func (m *Map) Store(key, value interface{}) { 409 | read, _ := m.read.Load().(readOnly) 410 | if e, ok := read.m[key]; ok && e.tryStore(&value) { 411 | return 412 | } 413 | 414 | m.mu.Lock() 415 | read, _ = m.read.Load().(readOnly) 416 | if e, ok := read.m[key]; ok { 417 | if e.unexpungeLocked() { 418 | // 将 expunged 转为 nil 419 | m.dirty[key] = e 420 | } 421 | e.storeLocked(&value) 422 | } else if e, ok := m.dirty[key]; ok { 423 | e.storeLocked(&value) 424 | } else { 425 | if !read.amended { 426 | // We're adding the first new key to the dirty map. 427 | // Make sure it is allocated and mark the read-only map as incomplete. 428 | m.dirtyLocked() 429 | m.read.Store(readOnly{m: read.m, amended: true}) 430 | } 431 | m.dirty[key] = newEntry(value) 432 | } 433 | m.mu.Unlock() 434 | } 435 | 436 | func (e *entry) tryStore(i *interface{}) bool { 437 | for { 438 | p := atomic.LoadPointer(&e.p) 439 | if p == expunged { 440 | return false 441 | } 442 | if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) { 443 | return true 444 | } 445 | } 446 | } 447 | 448 | func (e *entry) unexpungeLocked() (wasExpunged bool) { 449 | return atomic.CompareAndSwapPointer(&e.p, expunged, nil) 450 | } 451 | 452 | func (m *Map) dirtyLocked() { 453 | if m.dirty != nil { 454 | return 455 | } 456 | 457 | read, _ := m.read.Load().(readOnly) 458 | m.dirty = make(map[interface{}]*entry, len(read.m)) 459 | for k, e := range read.m { 460 | if !e.tryExpungeLocked() { // 将 nil 转为 expunged 状态,并且不会赋值给 dirty 461 | m.dirty[k] = e 462 | } 463 | } 464 | } 465 | 466 | func (e *entry) tryExpungeLocked() (isExpunged bool) { 467 | p := atomic.LoadPointer(&e.p) 468 | for p == nil { 469 | if atomic.CompareAndSwapPointer(&e.p, nil, expunged) { 470 | return true 471 | } 472 | p = atomic.LoadPointer(&e.p) 473 | } 474 | return p == expunged 475 | } 476 | ``` 477 | 478 | ### Delete 删除 479 | 480 | 删除很简单,read中存在,就把read中的entry.p置为nil,如果只在ditry中存在,那么就直接从dirty中删掉对应的entry。 481 | 482 | ``` 483 | func (m *Map) Delete(key interface{}) { 484 | read, _ := m.read.Load().(readOnly) 485 | e, ok := read.m[key] 486 | if !ok && read.amended { 487 | m.mu.Lock() 488 | read, _ = m.read.Load().(readOnly) 489 | e, ok = read.m[key] 490 | if !ok && read.amended { 491 | delete(m.dirty, key) 492 | } 493 | m.mu.Unlock() 494 | } 495 | if ok { 496 | e.delete() 497 | } 498 | } 499 | 500 | func (e *entry) delete() (hadValue bool) { 501 | for { 502 | p := atomic.LoadPointer(&e.p) 503 | if p == nil || p == expunged { 504 | return false 505 | } 506 | if atomic.CompareAndSwapPointer(&e.p, p, nil) { 507 | return true 508 | } 509 | } 510 | } 511 | 512 | ``` 513 | 514 | ## sync.waitgroup 515 | 516 | 我们先了解下 waitgroup 的用法,它最常用的场景是需要并发 n 个 G,需要等待 n 个 G 全部结束后,跑接下来的代码。 517 | 518 | - 首先会在当前 G 中调用 add 函数,传入一个数目 n; 519 | - 然后开始创建 Goroutine,并调用 wait 函数,阻塞 520 | - 每个 G 在将要结束的时候,调用 done 函数 521 | - 所有的 G 运行接受之后,n 被减为 0,这时候唤醒被 wait 阻塞的 G 522 | 523 | waitgroup 的原理也是十分简单,属性中有一个 state1 的 64 位数组,前 32 位代表着当前的 waitgroup 现有的counter 数目,后 32 为代表着调用 waitgroup.wait 的协程数目。 524 | 525 | - 当调用 add 函数的时候,前 32 为加上 n 数目 526 | - 当调用 done 函数的时候,前 32 为减 1 527 | - 当调用 wait 函数的时候,递增后 32 位,并且阻塞在 sema 信号量上 528 | - 当最后一个 G 调用 done 函数后,发现前 32 已经变成了 0,开始利用 sema 信号唤醒所有的等待者 529 | 530 | ``` 531 | // 在主 goroutine 中 Add 和 Wait,在其它 goroutine 中 Done 532 | // 在第一次使用之后,不能对 WaitGroup 再进行拷贝 533 | type WaitGroup struct { 534 | noCopy noCopy 535 | 536 | // state1 的高 32 位是计数器,低 32 位是 waiter 计数 537 | // 64 位的 atomic 操作需要按 64 位对齐,但是 32 位编译器没法保证这种对齐 538 | // 所以分配 12 个字节(多分配了 4 个字节) 539 | // 当 state 没有按 8 对齐时,我们可以偏 4 个字节来使用 540 | // 按 8 对齐时: 541 | // 0000...0000 0000...0000 0000...0000 542 | // |- 4 bytes-| |- 4 bytes -| |- 4 bytes -| 543 | // 使用 使用 不使用 544 | // 没有按 8 对齐时: 545 | // |- 4 bytes-| |- 4 bytes -| |- 4 bytes -| 546 | // 不使用 使用 使用 547 | // |-low-> ---------> ------> -----------> high-| 548 | state1 [12]byte 549 | sema uint32 550 | } 551 | 552 | func (wg *WaitGroup) state() *uint64 { 553 | // 判断 state 是否按照 8 字节对齐 554 | if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 { 555 | // 已对齐时,使用低 8 字节即可 556 | return (*uint64)(unsafe.Pointer(&wg.state1)) 557 | } else { 558 | // 未对齐时,使用高 8 字节 559 | return (*uint64)(unsafe.Pointer(&wg.state1[4])) 560 | } 561 | } 562 | 563 | // Add 一个 delta,delta 可能是负值,在 WaitGroup 的 counter 上增加该值 564 | // 如果 counter 变成 0,所有阻塞在 Wait 函数上的 goroutine 都会被释放 565 | // 如果 counter 变成了负数,Add 会直接 panic 566 | // 当 counter 是 0 且 Add 的 delta 为正的操作必须发生在 Wait 调用之前。 567 | // 而当 counter > 0 且 Add 的 delta 为负的操作则可以发生在任意时刻。 568 | // 一般来讲,Add 操作应该在创建 goroutine 或者其它需要等待的事件发生之前调用 569 | // 如果 wg 被用来等待几组独立的事件集合 570 | // 新的 Add 调用应该在所有 Wait 调用返回之后再调用 571 | // 参见 wg 的 example 572 | func (wg *WaitGroup) Add(delta int) { 573 | statep := wg.state() 574 | 575 | state := atomic.AddUint64(statep, uint64(delta)<<32) 576 | v := int32(state >> 32) // counter 高位 4 字节 577 | w := uint32(state) // waiter counter,截断,取低位 4 个字节 578 | 579 | if v < 0 { 580 | panic("sync: negative WaitGroup counter") 581 | } 582 | if w != 0 && delta > 0 && v == int32(delta) { 583 | panic("sync: WaitGroup misuse: Add called concurrently with Wait") 584 | } 585 | if v > 0 || w == 0 { 586 | return 587 | } 588 | 589 | // 当前 goroutine 已经把 counter 设为 0,且 waiter 数 > 0 590 | // 这时候不能有状态的跳变 591 | // - Add 不能和 Wait 进行并发调用 592 | // - Wait 如果发现 counter 已经等于 0,则不应该对 waiter 数加一了 593 | // 这里是对 wg 误用的简单检测 594 | if *statep != state { 595 | panic("sync: WaitGroup misuse: Add called concurrently with Wait") 596 | } 597 | 598 | // 此时 v 为 0,代表着可以唤醒了 599 | // 重置 waiter 计数为 0 600 | *statep = 0 601 | for ; w != 0; w-- { 602 | runtime_Semrelease(&wg.sema, false) 603 | } 604 | } 605 | 606 | // Done 其实就是 wg 的 counter - 1 607 | // 进入 Add 函数后 608 | // 如果 counter 变为 0 会触发 runtime_Semrelease 通知所有阻塞在 Wait 上的 g 609 | func (wg *WaitGroup) Done() { 610 | wg.Add(-1) 611 | } 612 | 613 | // Wait 会阻塞直到 wg 的 counter 变为 0 614 | func (wg *WaitGroup) Wait() { 615 | statep := wg.state() 616 | 617 | for { 618 | state := atomic.LoadUint64(statep) 619 | v := int32(state >> 32) // counter 620 | w := uint32(state) // waiter count 621 | if v == 0 { // counter 622 | return 623 | } 624 | 625 | // 如果没成功,可能有并发,循环再来一次相同流程 626 | // 成功直接返回 627 | if atomic.CompareAndSwapUint64(statep, state, state+1) { 628 | runtime_Semacquire(&wg.sema) // 和上面的 Add 里的 runtime_Semrelease 是对应的 629 | if *statep != 0 { 630 | panic("sync: WaitGroup is reused before previous Wait has returned") 631 | } 632 | return 633 | } 634 | } 635 | } 636 | 637 | ``` 638 | 639 | 不过新的代码又优化了数据结构 640 | 641 | 现在如果在 64 系统中,最后的 32bit 也不浪费,变成了信号量。 642 | 643 | 如果在 32 系统中,前 32bit 是信号量,后面 64bit 还是 counter 和 waiter。 644 | 645 | 代码原理没有大的变化。 646 | 647 | ``` 648 | type WaitGroup struct { 649 | noCopy noCopy 650 | 651 | // 64-bit value: high 32 bits are counter, low 32 bits are waiter count. 652 | // 64-bit atomic operations require 64-bit alignment, but 32-bit 653 | // compilers do not ensure it. So we allocate 12 bytes and then use 654 | // the aligned 8 bytes in them as state, and the other 4 as storage 655 | // for the sema. 656 | state1 [3]uint32 657 | } 658 | 659 | func (wg *WaitGroup) state() (statep *uint64, semap *uint32) { 660 | if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 { 661 | return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2] 662 | } else { 663 | return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0] 664 | } 665 | } 666 | ``` 667 | 668 | ## sync.once 669 | 670 | golang 的注释解释了为何不建议直接进行 CAS 操作,为何一定要用到锁 mutex,原因是如果 2 个 G 并发调用,要保证 2 个 G 返回的时候,F 已经执行完毕。如果按照注释那种代码,失败的 G 会立刻返回,但是此时 F 还未执行完毕。 671 | 672 | ``` 673 | type Once struct { 674 | done uint32 675 | m Mutex 676 | } 677 | 678 | func (o *Once) Do(f func()) { 679 | // Note: Here is an incorrect implementation of Do: 680 | // 681 | // if atomic.CompareAndSwapUint32(&o.done, 0, 1) { 682 | // f() 683 | // } 684 | // 685 | // Do guarantees that when it returns, f has finished. 686 | // This implementation would not implement that guarantee: 687 | // given two simultaneous calls, the winner of the cas would 688 | // call f, and the second would return immediately, without 689 | // waiting for the first's call to f to complete. 690 | // This is why the slow path falls back to a mutex, and why 691 | // the atomic.StoreUint32 must be delayed until after f returns. 692 | if atomic.LoadUint32(&o.done) == 0 { 693 | // Outlined slow-path to allow inlining of the fast-path. 694 | o.doSlow(f) 695 | } 696 | } 697 | 698 | func (o *Once) doSlow(f func()) { 699 | o.m.Lock() 700 | defer o.m.Unlock() 701 | if o.done == 0 { 702 | defer atomic.StoreUint32(&o.done, 1) 703 | f() 704 | } 705 | } 706 | ``` -------------------------------------------------------------------------------- /Go Sync——Mutex.md: -------------------------------------------------------------------------------- 1 | # Go Sync——Mutex 2 | 3 | [TOC] 4 | 5 | ## 基本原理 6 | 7 | 锁的整体设计有以下几点: 8 | 9 | - CAS原子操作。 10 | - 需要有一种阻塞和唤醒机制。 11 | - 尽量减少阻塞和唤醒切换成本。 12 | - 锁尽量公平,后来者要排队。即使被后来者插队了,也要照顾先来者,不能有“饥饿”现象。 13 | 14 | ### CAS 原子操作 15 | 16 | 只有通过 CAS 原子操作,我们才能够原子的更改 mutex 的状态,否则很有可能出现多个协程同时进入临界区的情况。 17 | 18 | ### 阻塞与唤醒 19 | 20 | 阻塞和唤醒机制是 mutex 必要的功能,这个 Golang 完全依赖信号量 sema。 21 | 22 | ### 自旋 spin 23 | 24 | 减少切换成本的方法就是不切换,简单而直接。不切换的方式就是让竞争者自旋。自旋一会儿,然后抢锁。不成功就再自旋。到达上限次数才阻塞。 25 | 26 | 不同平台上自旋所用的指令不一样。例如在amd64平台下,汇编的实现如下: 27 | 28 | ``` 29 | func sync_runtime_doSpin() { 30 | procyield(active_spin_cnt) 31 | } 32 | 33 | active_spin_cnt = 30 34 | 35 | TEXT runtime·procyield(SB),NOSPLIT,$0-0 36 | MOVL cycles+0(FP), AX 37 | again: 38 | // 自旋cycles次,每次自旋执行PAUSE指令 39 | PAUSE 40 | SUBL $1, AX 41 | JNZ again 42 | RET 43 | 44 | ``` 45 | 46 | 是否允许自旋的判断是严格的。而且最多自旋四次,每次30个CPU时钟周期。 47 | 48 | 能不能自旋全由这个条件语句决定 `if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter)`。 49 | 50 | ``` 51 | const active_spin = 4 52 | func sync_runtime_canSpin(i int) bool { 53 | // 自旋次数不能大于 active_spin(4) 次 54 | // cpu核数只有一个,不能自旋 55 | // 没有空闲的p了,不能自旋 56 | if i >= active_spin || ncpu <= 1 || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 { 57 | return false 58 | } 59 | // 当前g绑定的p里面本地待运行队列不为空,不能自旋 60 | if p := getg().m.p.ptr(); !runqempty(p) { 61 | return false 62 | } 63 | return true 64 | } 65 | 66 | ``` 67 | 68 | - 锁已被占用,并且锁不处于饥饿模式。 69 | - 积累的自旋次数小于最大自旋次数(active_spin=4)。 70 | - cpu核数大于1。 71 | - 有空闲的P。 72 | - 当前goroutine所挂载的P下,本地待运行队列为空。 73 | 74 | 可以看到自旋要求严格,毕竟在锁竞争激烈时,还无限制地自旋就肯定会影响其他goroutine。 75 | 76 | ### mutex 结构 77 | 78 | Mutex结构简单的就只有两个成员变量。sema是信号量。 79 | 80 | ``` 81 | type Mutex struct { 82 | // [阻塞的goroutine个数, starving标识, woken标识, locked标识] 83 | state int32 84 | sema uint32 85 | } 86 | ``` 87 | 88 | 这里主要介绍state的结构: 89 | 90 | ![](img/mutex.png) 91 | 92 | 一个32位的变量,被划分成上图的样子。右边的标识也有对应的常量: 93 | 94 | ``` 95 | const ( 96 | mutexLocked = 1 << iota // mutex is locked 97 | mutexWoken 98 | mutexStarving 99 | mutexWaiterShift = iota 100 | ) 101 | ``` 102 | 103 | 含义如下: 104 | 105 | - mutexLocked对应右边低位第一个bit。值为1,表示锁被占用。值为0,表示锁未被占用。 106 | - mutexWoken对应右边低位第二个bit。值为1,表示打上唤醒标记。值为0,表示没有唤醒标记。 107 | - mutexStarving对应右边低位第三个bit。值为1,表示锁处于饥饿模式。值为0,表示锁存于正常模式。 108 | - mutexWaiterShift是偏移量。它值为3。用法是state>>=mutexWaiterShift之后,state的值就表示当前阻塞等待锁的goroutine个数。最多可以阻塞2^29个goroutine。 109 | 110 | 111 | ## mutex 模式: 空闲/正常/饥饿/唤醒 112 | 113 | ### 空闲模式 114 | 115 | 在 Golang 中,抢锁实际上要先试图把 mutex 从 Null 状态转为 mutexLocked 状态: 116 | 117 | ``` 118 | func (m *Mutex) Lock() { 119 | // Fast path: grab unlocked mutex. 120 | if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { 121 | return 122 | } 123 | 124 | m.lockSlow() 125 | } 126 | 127 | ``` 128 | 值得注意的是这个 CAS 的初值为 0,这个是在 mutex 资源很空闲的情况,一步到位抢锁成功的情况。 129 | 130 | 但凡 mutex 进入了自旋、锁死、唤醒、饥饿等等状态,这个 CAS 操作都不会成功。 131 | 132 | ### 正常模式 133 | 134 | #### 自旋?/加锁?/CAS 成功? 135 | 136 | 正常模式下,对于新来的 goroutine 而言, 137 | 138 | - 发现此时 mutex 已经被锁住,它首先会尝试自旋, 139 | - 如果 mutex 并没有被锁,或者不符合自旋条件,直接尝试抢锁。 140 | - 符合自旋条件的,说明此时锁已经被占用,开始自旋。自旋过程中会设置 mutexWoken 标志,这样只要 unlock 过程中发现了 mutexWoken 标志,那么 unlock 就不会试图唤醒排队的 G,自旋的 G 可以立刻拿到锁。 141 | - 自旋结束的,取消 woken 状态 142 | - 如果锁此时是占用状态,那么就对 wait 自增。 143 | - 接下来,如果此时锁还没有被占用,那就开始试图利用 CAS 加锁。 144 | - 如果加锁失败,那说明存在并发的 lock 操作,重新开始即可 145 | - 此时 CAS 加锁成功,并且之前是未加锁状态,那么直接结束。 146 | 147 | ``` 148 | func (m *Mutex) lockSlow() { 149 | var waitStartTime int64 150 | starving := false 151 | awoke := false 152 | iter := 0 153 | old := m.state 154 | for { 155 | if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) { 156 | if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 && 157 | atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) { 158 | awoke = true 159 | } 160 | runtime_doSpin() 161 | iter++ 162 | old = m.state 163 | continue 164 | } 165 | 166 | new := old 167 | 168 | ... 169 | new |= mutexLocked 170 | 171 | if old&(mutexLocked) != 0 { 172 | new += 1 << mutexWaiterShift 173 | } 174 | 175 | if awoke { 176 | new &^= mutexWoken 177 | } 178 | 179 | if atomic.CompareAndSwapInt32(&m.state, old, new) { 180 | if old&(mutexLocked|mutexStarving) == 0 { 181 | break // locked the mutex with CAS 182 | } 183 | ... 184 | } else { 185 | old = m.state 186 | } 187 | } 188 | } 189 | ``` 190 | 191 | #### 阻塞 192 | 193 | CAS 操作成功之后: 194 | 195 | - 如果之前已经加锁了,CAS 只是增加等待的 G 个数,那么接下来就得考虑进行阻塞了 196 | - 首先更新 waitStartTime,代表第一次阻塞时间 197 | - runtime_SemacquireMutex 利用信号量进行阻塞,由于是第一次阻塞,直接放到等待队列的尾部即可。 198 | 199 | ``` 200 | if atomic.CompareAndSwapInt32(&m.state, old, new) { 201 | // If we were already waiting before, queue at the front of the queue. 202 | queueLifo := 0 203 | if waitStartTime == 0 { 204 | waitStartTime = runtime_nanotime() 205 | } 206 | 207 | runtime_SemacquireMutex(&m.sema, queueLifo, 1) 208 | 209 | ... 210 | } 211 | 212 | ``` 213 | 214 | #### Unlock 解锁 215 | 216 | - 解锁之后,如果发现 state 直接为 0 了,说明没有 G 等待着 mutex,直接返回即可。 217 | - 为 mutex.state 添加 mutexWoken 标志,试图让自旋的 G 快速获得 mutex。 218 | - 不断循环直到成功,或者期间其他 G 自旋或者加锁成功。 219 | 220 | ``` 221 | func (m *Mutex) Unlock() { 222 | // Fast path: drop lock bit. 223 | new := atomic.AddInt32(&m.state, -mutexLocked) 224 | if new != 0 { 225 | // Outlined slow path to allow inlining the fast path. 226 | // To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock. 227 | m.unlockSlow(new) 228 | } 229 | } 230 | 231 | func (m *Mutex) unlockSlow(new int32) { 232 | if new&mutexStarving == 0 { // 非饥饿状态 233 | old := new 234 | for { 235 | // 没有等待的 G,直接返回 236 | // 如果此时 mutexWoken 标志已经被置 1,那么让自旋的 G 抢到锁,不需要从等待队列中去取 237 | // 如果此时锁已经被占用,那说明有新的 G 抢到了锁,直接返回 238 | if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken) != 0 { 239 | return 240 | } 241 | 242 | // 加入 mutexWoken 标志,表明当前锁并不是空闲状态,因此此时还有 G 在等待着锁 243 | new = (old - 1< starvationThresholdNs 287 | old = m.state 288 | ... 289 | awoke = true 290 | iter = 0 291 | } 292 | } 293 | 294 | ``` 295 | 296 | ### 饥饿模式 297 | 298 | 饥饿模式下,对于新来的goroutine,它只有一个选择,就是追加到阻塞队列尾部,等待被唤醒的。而且在该模式下,所有锁竞争者都不能自旋。 299 | 300 | #### 饥饿模式 lock 301 | 302 | - 饥饿模式不允许自旋, 303 | - 饥饿模式也不允许试图加锁 304 | - 饥饿模式下,只能递增 mutex 的 wait 数量 305 | - 将当前 G 阻塞 306 | 307 | ``` 308 | func (m *Mutex) lockSlow() { 309 | old := m.state 310 | for { 311 | new := old 312 | if old&(mutexLocked|mutexStarving) != 0 { 313 | new += 1 << mutexWaiterShift 314 | } 315 | 316 | if atomic.CompareAndSwapInt32(&m.state, old, new) { 317 | runtime_SemacquireMutex(&m.sema, queueLifo, 1) 318 | 319 | ... 320 | } else { 321 | old = m.state 322 | } 323 | } 324 | } 325 | 326 | ``` 327 | 328 | #### 饥饿模式 unlock 329 | 330 | 饥饿模式下,调用 runtime_Semrelease,并且使用 handoff 参数唤醒等待队列,handoff 的作用就是在调度等待队列的时候,确保其他 G 调用 runtime_SemacquireMutex 会被阻塞。 331 | 332 | ``` 333 | func (m *Mutex) unlockSlow(new int32) { 334 | if (new+mutexLocked)&mutexLocked == 0 { 335 | throw("sync: unlock of unlocked mutex") 336 | } 337 | if new&mutexStarving == 0 { 338 | ... 339 | } else { 340 | runtime_Semrelease(&m.sema, true, 1) 341 | } 342 | } 343 | 344 | ``` 345 | 346 | #### 饥饿模式唤醒 347 | 348 | 处于饥饿模式的 G 重新被唤醒之后,如果 mutex 的等待队列为空,那么就取消饥饿模式。 349 | 350 | ``` 351 | if atomic.CompareAndSwapInt32(&m.state, old, new) { 352 | ... 353 | runtime_SemacquireMutex(&m.sema, queueLifo, 1) 354 | starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs 355 | old = m.state 356 | if old&mutexStarving != 0 { 357 | delta := int32(mutexLocked - 1<>mutexWaiterShift == 1 { 359 | delta -= mutexStarving 360 | } 361 | atomic.AddInt32(&m.state, delta) 362 | break 363 | } 364 | awoke = true 365 | iter = 0 366 | } 367 | 368 | ``` 369 | 370 | ## 完全版 371 | 372 | 373 | ``` 374 | func (m *Mutex) Lock() { 375 | // 尝试CAS上锁 376 | if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { 377 | return 378 | } 379 | // 上锁成功,直接返回 380 | m.lockSlow() 381 | } 382 | 383 | func (m *Mutex) lockSlow() { 384 | var waitStartTime int64 385 | starving := false 386 | awoke := false 387 | iter := 0 388 | old := m.state 389 | for { 390 | // Don't spin in starvation mode, ownership is handed off to waiters 391 | // so we won't be able to acquire the mutex anyway. 392 | if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) { 393 | // Active spinning makes sense. 394 | // Try to set mutexWoken flag to inform Unlock 395 | // to not wake other blocked goroutines. 396 | if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 && 397 | atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) { 398 | awoke = true 399 | } 400 | runtime_doSpin() 401 | iter++ 402 | old = m.state 403 | continue 404 | } 405 | new := old 406 | // Don't try to acquire starving mutex, new arriving goroutines must queue. 407 | if old&mutexStarving == 0 { 408 | new |= mutexLocked 409 | } 410 | if old&(mutexLocked|mutexStarving) != 0 { 411 | new += 1 << mutexWaiterShift 412 | } 413 | // The current goroutine switches mutex to starvation mode. 414 | // But if the mutex is currently unlocked, don't do the switch. 415 | // Unlock expects that starving mutex has waiters, which will not 416 | // be true in this case. 417 | if starving && old&mutexLocked != 0 { 418 | new |= mutexStarving 419 | } 420 | if awoke { 421 | // The goroutine has been woken from sleep, 422 | // so we need to reset the flag in either case. 423 | if new&mutexWoken == 0 { 424 | throw("sync: inconsistent mutex state") 425 | } 426 | new &^= mutexWoken 427 | } 428 | if atomic.CompareAndSwapInt32(&m.state, old, new) { 429 | if old&(mutexLocked|mutexStarving) == 0 { 430 | break // locked the mutex with CAS 431 | } 432 | // If we were already waiting before, queue at the front of the queue. 433 | queueLifo := waitStartTime != 0 434 | if waitStartTime == 0 { 435 | waitStartTime = runtime_nanotime() 436 | } 437 | runtime_SemacquireMutex(&m.sema, queueLifo, 1) 438 | starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs 439 | old = m.state 440 | if old&mutexStarving != 0 { 441 | // If this goroutine was woken and mutex is in starvation mode, 442 | // ownership was handed off to us but mutex is in somewhat 443 | // inconsistent state: mutexLocked is not set and we are still 444 | // accounted as waiter. Fix that. 445 | if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 { 446 | throw("sync: inconsistent mutex state") 447 | } 448 | delta := int32(mutexLocked - 1<>mutexWaiterShift == 1 { 450 | // Exit starvation mode. 451 | // Critical to do it here and consider wait time. 452 | // Starvation mode is so inefficient, that two goroutines 453 | // can go lock-step infinitely once they switch mutex 454 | // to starvation mode. 455 | delta -= mutexStarving 456 | } 457 | atomic.AddInt32(&m.state, delta) 458 | break 459 | } 460 | awoke = true 461 | iter = 0 462 | } else { 463 | old = m.state 464 | } 465 | } 466 | } 467 | ``` 468 | 469 | ## sync.RWMutex 470 | 471 | 读写锁的设计比较简单,它的实现是建立在 mutex 锁的基础上的。 472 | 473 | 它的原理也很简单: 474 | 475 | - 加读锁:只需要递增 readerCount 即可,无需加锁,因为读锁是允许并发读的 476 | - 解读锁:只需要递减 readerCount 即可。 477 | - 加写锁: 478 | - 因为写锁是独占的,所以就需要先利用 w.lock 加锁。 479 | - 加锁成功之后,首先要将 readerCount 减去一个常量,代表着这个时候有写锁在等待,更新 readerWait 的值为此时 readerCount 的值,代表着位于读锁前面的,写锁要等待的读锁的个数。 480 | - 使用 writerSem 信号量阻塞当前的 G 481 | - 加读锁:这个时候如果再想加读锁,就没有那么简单了。这时候发现 readerCount 小于 0,那么就说明此时有写锁,此时仍然递增 readerCount,告诉写锁,自己被阻塞了。然后利用信号量 readerSem 阻塞住当前的 Goroutine。 482 | - 解读锁:递减 readerCount 后发现 readerCount 小于 0,说明我们在临界区的时候,有写锁尝试加锁失败,那么此时我们还需要递减 readerWait,如果 readerWait 为 0 了,说明写锁前面的读锁已经全部处理完毕,使用 writerSem 信号量唤醒写锁所在的 G 483 | - 解写锁:当写锁保护的临界区完毕之后,readerCount 代表着所有等待着写锁的读锁个数,使用 readerSem 唤醒这些阻塞的 G 484 | 485 | 486 | ``` 487 | type RWMutex struct { 488 | w Mutex // held if there are pending writers 489 | writerSem uint32 // semaphore for writers to wait for completing readers 490 | readerSem uint32 // semaphore for readers to wait for completing writers 491 | readerCount int32 // number of pending readers 492 | readerWait int32 // number of departing readers 493 | } 494 | 495 | func (rw *RWMutex) RLock() { 496 | if atomic.AddInt32(&rw.readerCount, 1) < 0 { 497 | // A writer is pending, wait for it. 498 | runtime_SemacquireMutex(&rw.readerSem, false, 0) 499 | } 500 | } 501 | 502 | func (rw *RWMutex) RUnlock() { 503 | if race.Enabled { 504 | _ = rw.w.state 505 | race.ReleaseMerge(unsafe.Pointer(&rw.writerSem)) 506 | race.Disable() 507 | } 508 | if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 { 509 | // Outlined slow-path to allow the fast-path to be inlined 510 | rw.rUnlockSlow(r) 511 | } 512 | if race.Enabled { 513 | race.Enable() 514 | } 515 | } 516 | 517 | func (rw *RWMutex) rUnlockSlow(r int32) { 518 | if r+1 == 0 || r+1 == -rwmutexMaxReaders { 519 | race.Enable() 520 | throw("sync: RUnlock of unlocked RWMutex") 521 | } 522 | // A writer is pending. 523 | if atomic.AddInt32(&rw.readerWait, -1) == 0 { 524 | // The last reader unblocks the writer. 525 | runtime_Semrelease(&rw.writerSem, false, 1) 526 | } 527 | } 528 | 529 | func (rw *RWMutex) Lock() { 530 | rw.w.Lock() 531 | // Announce to readers there is a pending writer. 532 | r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders 533 | // Wait for active readers. 534 | if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 { 535 | runtime_SemacquireMutex(&rw.writerSem, false, 0) 536 | } 537 | } 538 | 539 | 540 | func (rw *RWMutex) Unlock() { 541 | // Announce to readers there is no active writer. 542 | r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders) 543 | 544 | // Unblock blocked readers, if any. 545 | for i := 0; i < int(r); i++ { 546 | runtime_Semrelease(&rw.readerSem, false, 0) 547 | } 548 | // Allow other writers to proceed. 549 | rw.w.Unlock() 550 | } 551 | 552 | ``` -------------------------------------------------------------------------------- /Go interface.md: -------------------------------------------------------------------------------- 1 | # Go interface 2 | 3 | [TOC] 4 | 5 | ## 基本用法 6 | 7 | ### 指针和接口 8 | 9 | #### 结构体的函数 10 | 11 | Go 语言是一个有指针类型的编程语言,当指针和接口同时出现时就会遇到一些让人困惑或者感到诡异的问题,接口在定义一组方法时其实没有对实现的接受者做限制,所以我们其实会在一个类型上看到以下两种不同的实现方式: 12 | 13 | ![](img/interface.png) 14 | 15 | - addressable 变量的函数调用 16 | 17 | 对于函数来说,只要变量是 addressable 的,无所谓变量是 T 还是 `*T`,也无所谓函数的接受者是 T 还是 `*T`,编译器都会进行优化: 18 | 19 | 对于 Cat 结构体来说,无论函数定义为结构体类型还是指针类型,被初始化为结构体还是指针,它都能直接调用: 20 | 21 | ``` 22 | type Cat struct{} 23 | 24 | func (c Cat) Walk() { 25 | fmt.Println("catwalk") 26 | } 27 | func (c Cat) Quack() { 28 | fmt.Println("meow") 29 | } 30 | 31 | func main() { 32 | var t Cat 33 | t.Walk() 34 | t.Quack() 35 | 36 | var d = &t 37 | d.Walk() 38 | d.Quack() 39 | } 40 | ``` 41 | 42 | 43 | ``` 44 | type Cat struct{} 45 | 46 | func (c *Cat) Walk() { 47 | fmt.Println("catwalk") 48 | } 49 | func (c *Cat) Quack() { 50 | fmt.Println("meow") 51 | } 52 | 53 | func main() { 54 | var t Cat 55 | t.Walk() 56 | t.Quack() 57 | 58 | var d = &t 59 | d.Walk() 60 | d.Quack() 61 | } 62 | 63 | ``` 64 | 65 | - 非 addressable 变量的函数调用 66 | 67 | 如果使用类似右值的方式调用的话,情况有些不太相同。 68 | 69 | 如果函数被定义为结构体,右值不管怎么调用都可以。 70 | 71 | ``` 72 | type Cat struct{} 73 | 74 | func (c Cat) Walk() { 75 | fmt.Println("catwalk") 76 | } 77 | func (c Cat) Quack() { 78 | fmt.Println("meow") 79 | } 80 | 81 | func main() { 82 | Cat{}.Walk() 83 | Cat{}.Quack() 84 | 85 | (&Cat{}).Walk() 86 | (&Cat{}).Quack() 87 | } 88 | ``` 89 | 90 | 但是如果函数被定义为指针的话,就比较麻烦, 这个代码编译之后,会报错: 91 | 92 | ``` 93 | type Cat struct{} 94 | 95 | func (c *Cat) Walk() { 96 | fmt.Println("catwalk") 97 | } 98 | func (c *Cat) Quack() { 99 | fmt.Println("meow") 100 | } 101 | 102 | func main() { 103 | Cat{}.Walk() 104 | Cat{}.Quack() 105 | 106 | (&Cat{}).Walk() 107 | (&Cat{}).Quack() 108 | } 109 | 110 | ./test.go:20:7: cannot call pointer method on Cat literal 111 | ./test.go:20:7: cannot take the address of Cat literal 112 | 113 | ``` 114 | 原因就是右值匿名结构体可以看做是个只读的变量值,是不允许取到地址的,因此无法调用指针类型的函数。 115 | 116 | 但是我们在调用之前,先去取地址,类似 `(&Cat{}).Walk()` 这个代码是没有问题的,编译器将在堆中构建 Cat 结构体,将地址存放到栈里,不会把它看做右值。 117 | 118 | #### 接口的函数 119 | 120 | 对于接口和变量的转换来说,是否可以转换成功就不是是否可以 addressable 可以决定的了。决定是否可以转换的关键是 [Method sets](https://golang.org/ref/spec#Method_sets)。 121 | 122 | Method sets 规定 `*T` 可以访问所有的 `*T` 和 T 的方法集,而 T 只能访问 T 的方法集。 123 | 124 | - *T 赋值 125 | 126 | 对于 *T 来说,它可以接收所有的函数,无论接受者是什么: 127 | 128 | ``` 129 | type Duck interface { 130 | Walk() 131 | Quack() 132 | } 133 | 134 | type Cat struct{} 135 | 136 | func (c Cat) Walk() { 137 | fmt.Println("catwalk") 138 | } 139 | func (c Cat) Quack() { 140 | fmt.Println("meow") 141 | } 142 | 143 | func main() { 144 | var c Duck = &Cat{} 145 | c.Walk() 146 | c.Quack() 147 | 148 | var t Cat 149 | var d Duck = &t 150 | d.Walk() 151 | d.Quack() 152 | } 153 | 154 | ``` 155 | 156 | ``` 157 | type Duck interface { 158 | Walk() 159 | Quack() 160 | } 161 | 162 | type Cat struct{} 163 | 164 | func (c *Cat) Walk() { 165 | fmt.Println("catwalk") 166 | } 167 | func (c *Cat) Quack() { 168 | fmt.Println("meow") 169 | } 170 | 171 | func main() { 172 | var c Duck = &Cat{} 173 | c.Walk() 174 | c.Quack() 175 | 176 | var t Cat 177 | var d Duck = &t 178 | d.Walk() 179 | d.Quack() 180 | } 181 | 182 | ``` 183 | 184 | - T 赋值 185 | 186 | T 可以访问接受者为 T 的函数,因此可以转换为相应的 interface 成功 187 | 188 | ``` 189 | type Duck interface { 190 | Walk() 191 | Quack() 192 | } 193 | 194 | type Cat struct{} 195 | 196 | func (c Cat) Walk() { 197 | fmt.Println("catwalk") 198 | } 199 | func (c Cat) Quack() { 200 | fmt.Println("meow") 201 | } 202 | 203 | func main() { 204 | var t Cat 205 | var c Duck = t 206 | c.Walk() 207 | c.Quack() 208 | 209 | var d Duck = Cat{} 210 | d.Walk() 211 | d.Quack() 212 | } 213 | ``` 214 | 215 | 但是,和结构体的函数调用不同的是,T 无法调用 `*T` 的函数,因此 T 并没有接受者为 `*T` 的 Walk/Quack 方法,因此它无法转换为 Duck 接口。 216 | 217 | ``` 218 | type Duck interface { 219 | Walk() 220 | Quack() 221 | } 222 | 223 | type Cat struct{} 224 | 225 | func (c *Cat) Walk() { 226 | fmt.Println("catwalk") 227 | } 228 | func (c *Cat) Quack() { 229 | fmt.Println("meow") 230 | } 231 | 232 | func main() { 233 | var t Cat 234 | var c Duck = t 235 | c.Walk() 236 | c.Quack() 237 | 238 | var d Duck = Cat{} 239 | d.Walk() 240 | d.Quack() 241 | } 242 | 243 | ./test.go:21:6: cannot use t (type Cat) as type Duck in assignment: 244 | Cat does not implement Duck (Quack method has pointer receiver) 245 | 246 | ``` 247 | 248 | 编译器会提醒我们『Cat 类型并没有实现 Duck 接口,Quack 方法的接受者是指针』,这两种情况其实非常让人困惑,尤其是对于刚刚接触 Go 语言接口的开发者,想要理解这个问题,首先要知道 Go 语言在进行 参数传递 时都是值传递的。 249 | 250 | ![](img/methodset.png) 251 | 252 | 官方文档写的比较清楚,原因有两个: 253 | 254 | - 一个是有些临时变量是无法 addressable 的,这部分变量不允许去取变量的地址,自然没有办法调用 `*T` 的函数 255 | - 另一个原因是,即使是可以 addressable 的变量 S,如果调用 `*T` 的方法目的是改变变量 S 的内部属性值,但是偏偏 interface 的转化过程是复制一份变量 S1(`var d Duck = S` 实际上是复制了一份 S 到 d 的内部属性 d.data 中),导致改变的也仅仅是 S1,造成了歧义。这里就是 golang 官方为了避免歧义,在接口的转化过程中,直接禁止 `T` 拥有 `*T` 的函数。如果想要改变 S 变量,请传递指针变量,`var d Duck = &S`,这样 interface 复制的就是 *S 的指针地址,调用函数才能真正的更改 S 的内部属性值。 256 | - 对于函数调用来说,如果 T 调用了 (`*T`) 的方法,编译器直接就对 T 进行了取地址的操作;而 interface 在转化阶段因为采取了复制的操作,导致了反直觉的效果。因此这两个采取的策略是不同的。 257 | 258 | 一般来说,我们提倡声明方法时使用指针,隐式或者显示转化为 interface 的时候也使用指针,可以避免对象的复制。 259 | 260 | ### nil 和 non-nil 261 | 262 | 我们可以通过一个例子理解『Go 语言的接口类型不是任意类型』这一句话,下面的代码在 main 函数中初始化了一个 *TestStruct 结构体指针,由于指针的零值是 nil,所以变量 s 在初始化之后也是 nil: 263 | 264 | ``` 265 | package main 266 | 267 | type TestStruct struct{} 268 | 269 | func NilOrNot(v interface{}) { 270 | if v == nil { 271 | println("nil") 272 | } else { 273 | println("non-nil") 274 | } 275 | } 276 | 277 | func main() { 278 | var s *TestStruct 279 | NilOrNot(s) 280 | } 281 | 282 | $ go run main.go 283 | non-nil 284 | 285 | ``` 286 | 287 | 但是当我们将 s 变量传入 NilOrNot 时,该方法却打印出了 non-nil 字符串,这主要是因为调用 NilOrNot 函数时其实会发生隐式的类型转换,变量 nil 会被转换成 interface{} 类型,interface{} 类型是一个结构体,它除了包含 nil 变量之外还包含变量的类型信息,也就是 TestStruct,所以在这里会打印出 non-nil,我们会在接下来详细介绍结构的实现原理。 288 | 289 | ## _type 290 | 291 | ### _type 292 | 293 | ![](img/_type.png) 294 | 295 | 在Go语言中_type这个结构体非常重要,记录着某种数据类型的一些基本特征,比如这个数据类型占用的内存大小(size字段),数据类型的名称(nameOff字段)等等。每种数据类型都存在一个与之对应的_type结构体。 296 | 297 | ``` 298 | //src/runtime/type.go 299 | type type struct { 300 | size uintptr // 大小 301 | ptrdata uintptr //size of memory prefix holding all pointers 302 | hash uint32 //类型Hash 303 | tflag tflag //类型的特征标记 304 | align uint8 //_type 作为整体交量存放时的对齐字节数 305 | fieldalign uint8 //当前结构字段的对齐字节数 306 | kind uint8 //基础类型枚举值和反射中的 Kind 一致,kind 决定了如何解析该类型 307 | alg *typeAlg //指向一个函数指针表,该表有两个函数,一个是计算类型 Hash 函 308 | //数,另一个是比较两个类型是否相同的 equal 函数 309 | //gcdata stores the GC type data for the garbage collector. 310 | //If the KindGCProg bit is set in kind, gcdata is a GC program. 311 | //Otherwise it is a ptrmask bitmap. See mbitmap.go for details. 312 | gcdata *byte //GC 相关信息 313 | str nameOff //str 用来表示类型名称字符串在编译后二进制文件中某个 section 314 | //的偏移量 315 | //由链接器负责填充 316 | ptrToThis typeOff //ptrToThis 用来表示类型元信息的指针在编译后二进制文件中某个 317 | //section 的偏移量 318 | //由链接器负责填充 319 | } 320 | 321 | ``` 322 | 323 | - size 为该类型所占用的字节数量。 324 | - kind 表示类型的种类,如 bool、int、float、string、struct、interface 等。 325 | - str 表示类型的名字信息,它是一个 nameOff(int32) 类型,通过这个 nameOff,可以找到类型的名字字符串 326 | 327 | _type 包含所有类型的共同元信息,编译器和运行时可以根据该元信息解析具体类型、类型名存放位置、类型的 Hash 值等基本信息。 328 | 329 | 这里需要说明一下:_type 里面的 nameOff 和 typeOff 最终是由链接器负责确定和填充的,它们都是一个偏移量(offset),类型的名称和类型元信息实际上存放在连接后可执行文件的某个段(section)里,这两个值是相对于段内的偏移量,运行时提供两个转换查找函数。 330 | 331 | ### extras 332 | 333 | 如果是一些比较特殊的数据类型,可能还会对_type结构体进行扩展,记录更多的信息,我们可以称之为 extras。extras 对于基础类型(如 bool,int, float 等)是 size 为 0 的,它为复杂的类型提供了一些额外信息。例如为 struct 类型提供 structtype,为 slice 类型提供 slicetype 等信息。 334 | 335 | ``` 336 | type arraytype struct { 337 | typ _type 338 | elem *_type 339 | slice *_type 340 | len uintptr 341 | } 342 | 343 | type chantype struct { 344 | typ _type 345 | elem *_type 346 | dir uintptr 347 | } 348 | 349 | type slicetype struct { 350 | typ _type 351 | elem *_type 352 | } 353 | 354 | type functype struct { 355 | typ _type 356 | inCount uint16 357 | outCount uint16 358 | } 359 | 360 | type ptrtype struct { 361 | typ _type 362 | elem *_type 363 | } 364 | 365 | type structtype struct { 366 | typ _type 367 | pkgPath name 368 | fields []structfield 369 | } 370 | 371 | type structfield struct { 372 | name name 373 | typ *_type 374 | offsetAnon uintptr 375 | } 376 | 377 | type name struct { 378 | bytes *byte 379 | } 380 | 381 | ``` 382 | 383 | ### uncommontype 384 | 385 | 处理 extras 之外,还存在着 uncommon 字段的类型,ucom 对于基础类型也是 size 为 0 的,但是对于 type Binary int 这种定义或者是其它复杂类型来说,ucom 用来存储类型的函数列表等信息。 386 | 387 | ``` 388 | type uncommontype struct { 389 | pkgpath nameOff 390 | mcount uint16 // number of methods 391 | xcount uint16 // number of exported methods 392 | moff uint32 // offset from this uncommontype to [mcount]method 393 | _ uint32 // unused 394 | } 395 | 396 | ``` 397 | 398 | 我们可以看看 golang 如何提取类型中的 uncommon 字段: 399 | 400 | ``` 401 | func (t *_type) uncommon() *uncommontype { 402 | if t.tflag&tflagUncommon == 0 { 403 | return nil 404 | } 405 | switch t.kind & kindMask { 406 | case kindStruct: 407 | type u struct { 408 | structtype 409 | u uncommontype 410 | } 411 | return &(*u)(unsafe.Pointer(t)).u 412 | case kindPtr: 413 | type u struct { 414 | ptrtype 415 | u uncommontype 416 | } 417 | return &(*u)(unsafe.Pointer(t)).u 418 | case kindFunc: 419 | type u struct { 420 | functype 421 | u uncommontype 422 | } 423 | return &(*u)(unsafe.Pointer(t)).u 424 | case kindSlice: 425 | type u struct { 426 | slicetype 427 | u uncommontype 428 | } 429 | return &(*u)(unsafe.Pointer(t)).u 430 | case kindArray: 431 | type u struct { 432 | arraytype 433 | u uncommontype 434 | } 435 | return &(*u)(unsafe.Pointer(t)).u 436 | case kindChan: 437 | type u struct { 438 | chantype 439 | u uncommontype 440 | } 441 | return &(*u)(unsafe.Pointer(t)).u 442 | case kindMap: 443 | type u struct { 444 | maptype 445 | u uncommontype 446 | } 447 | return &(*u)(unsafe.Pointer(t)).u 448 | case kindInterface: 449 | type u struct { 450 | interfacetype 451 | u uncommontype 452 | } 453 | return &(*u)(unsafe.Pointer(t)).u 454 | default: 455 | type u struct { 456 | _type 457 | u uncommontype 458 | } 459 | return &(*u)(unsafe.Pointer(t)).u 460 | } 461 | } 462 | 463 | ``` 464 | 465 | ## eface 与 iface 466 | 467 | ### eface 468 | 469 | 不包含任何方法的 interface{} 类型在底层其实就是 eface 结构体,我们先来看 eface 结构体的组成: 470 | 471 | 472 | ``` 473 | type eface struct { // 16 bytes 474 | _type *_type 475 | data unsafe.Pointer 476 | } 477 | 478 | ``` 479 | 480 | 由于 interface{} 类型不包含任何方法,所以它的结构也相对来说比较简单,只包含指向底层数据和类型的两个指针,从这里的结构我们也就能够推断出: 任意的类型都可以转换成 interface{} 类型。 481 | 482 | ### iface 483 | 484 | 另一个用于表示接口 interface 类型的结构体就是 iface 了,在这个结构体中也有指向原始数据的指针 data,在这个结构体中更重要的其实是 itab 类型的 tab 字段。 485 | 486 | ``` 487 | type iface struct { // 16 bytes 488 | tab *itab 489 | data unsafe.Pointer 490 | } 491 | 492 | ``` 493 | 494 | ### itab 结构体 495 | 496 | itab 结构体是接口类型的核心组成部分,每一个 itab 都占 32 字节的空间。 497 | 498 | _type 实际上是 iface 实际的对象类型。 499 | 500 | itab 结构体中还包含另一个表示接口类型的 interfacetype 字段,它就是一个对 _type 类型的简单封装,属于我们上面所说的 `_type` 的 extras 字段。 501 | 502 | hash 字段其实是对 `_type.hash` 的拷贝,它会在从 interface 到具体类型的切换时用于快速判断目标类型和接口中类型是否一致;最后的 fun 数组其实是一个动态大小的数组,如果当前数组中内容为空就表示 `_type` 没有实现 inter 接口,虽然这是一个大小固定的数组,但是在使用时会直接通过指针获取其中的数据并不会检查数组的边界,所以该数组中保存的元素数量是不确定的。 503 | 504 | ``` 505 | type itab struct { 506 | inter *interfacetype 507 | _type *_type 508 | hash uint32 // copy of _type.hash. Used for type switches. 509 | _ [4]byte 510 | fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter. 511 | } 512 | 513 | type interfacetype struct { 514 | typ _type 515 | pkgpath name 516 | mhdr []imethod 517 | } 518 | ``` 519 | 520 | 521 | ## 接口的转换 522 | 523 | ### 指针类型 524 | 525 | ``` 526 | package main 527 | 528 | type Duck interface { 529 | Quack() 530 | } 531 | 532 | type Cat struct { 533 | Name string 534 | } 535 | 536 | //go:noinline 537 | func (c *Cat) Quack() { 538 | println(c.Name + " meow") 539 | } 540 | 541 | func main() { 542 | var c Duck = &Cat{Name: "grooming"} 543 | c.Quack() 544 | } 545 | ``` 546 | 547 | 将上述代码编译成汇编语言之后,我们删掉其中一些对理解接口原理无用的指令,只保留与赋值语句 var c Duck = &Cat{Name: "grooming"} 相关的代码,先来了解一下结构体指针被装到接口变量 c 的过程: 548 | 549 | ``` 550 | LEAQ type."".Cat(SB), AX 551 | MOVQ AX, (SP) 552 | CALL runtime.newobject(SB) 553 | MOVQ 8(SP), DI 554 | MOVQ $8, 8(DI) 555 | LEAQ go.string."grooming"(SB), AX 556 | MOVQ AX, (DI) 557 | LEAQ go.itab.*"".Cat,"".Duck(SB), AX 558 | TESTB AL, (AX) 559 | MOVQ DI, (SP) 560 | 561 | ``` 562 | 563 | 这段代码的第一部分其实就是对 Cat 结构体的初始化,我们直接展示上述汇编语言对应的伪代码,帮助我们更快地理解这个过程: 564 | 565 | ``` 566 | LEAQ type."".Cat(SB), AX ;; AX = &type."".Cat 567 | MOVQ AX, (SP) ;; SP = &type."".Cat 568 | CALL runtime.newobject(SB) ;; SP + 8 = &Cat{} 569 | MOVQ 8(SP), DI ;; DI = &Cat{} 570 | MOVQ $8, 8(DI) ;; StringHeader(DI.Name).Len = 8 571 | LEAQ go.string."grooming"(SB), AX ;; AX = &"grooming" 572 | MOVQ AX, (DI) ;; StringHeader(DI.Name).Data = &"grooming" 573 | 574 | ``` 575 | 576 | - 获取 Cat 结构体类型指针并将其作为参数放到栈 SP 上; 577 | - 通过 CALL 指定调用 runtime.newobject 函数,这个函数会以 Cat 结构体类型指针作为入参,分配一片新的内存空间并将指向这片内存空间的指针返回到 SP+8 上; 578 | - SP+8 现在存储了一个指向 Cat 结构体的指针,我们将栈上的指针拷贝到寄存器 DI 上方便操作; 579 | - 由于 Cat 中只包含一个字符串类型的 Name 变量,所以在这里会分别将字符串地址 &"grooming" 和字符串长度 8 设置到结构体上,最后三行汇编指令的作用就等价于 cat.Name = "grooming"; 580 | 581 | 字符串在运行时的表示其实就是指针加上字符串长度,我们这里要看一下初始化之后的 Cat 结构体在内存中的表示是什么样的: 582 | 583 | ![](img/cat.png) 584 | 585 | 每一个 Cat 结构体在内存中的大小都是 16 字节,这是因为其中只包含一个字符串字段,而字符串在 Go 语言中总共占 16 字节,初始化 Cat 结构体之后就进入了将 *Cat 转换成 Duck 类型的过程了: 586 | 587 | ``` 588 | LEAQ go.itab.*"".Cat,"".Duck(SB), AX ;; AX = *itab(go.itab.*"".Cat,"".Duck) 589 | MOVQ AX, (SP) ;; SP = AX 590 | CALL "".(*Cat).Quack(SB) ;; SP.Quack() 591 | 592 | ``` 593 | Duck 作为一个包含方法的接口,它在底层就会使用 iface 结构体进行表示,iface 结构体包含两个字段,其中一个是指向数据的指针,另一个是表示接口和结构体关系的 tab 字段,我们已经通过上一段代码在栈上的 SP+8 初始化了 Cat 结构体指针,这段代码其实只是将编译期间生成的 itab 结构体指针复制到 SP 上: 594 | 595 | ![](img/cat1.png) 596 | 597 | 我们会发现 SP 和 SP+8 总共 16 个字节共同组成了 iface 结构体,栈上的这个 iface 结构体也就是 Quack 方法的第一个入参。 598 | 599 | 到这里已经完成了对 Cat 指针转换成 iface 结构体并调用 Quack 方法过程的分析,我们再重新回顾一下整个调用过程的汇编代码和伪代码,其中的大部分内容都是对 Cat 指针和 iface 的初始化,调用 Quack 方法时其实也只执行了一个汇编指令,调用的过程也没有经过动态派发的过程,这其实就是 Go 语言编译器帮我们做的优化了. 600 | 601 | ### 结构体类型 602 | 603 | ``` 604 | package main 605 | 606 | type Duck interface { 607 | Quack() 608 | } 609 | 610 | type Cat struct { 611 | Name string 612 | } 613 | 614 | //go:noinline 615 | func (c Cat) Quack() { 616 | println(c.Name + " meow") 617 | } 618 | 619 | func main() { 620 | var c Duck = Cat{Name: "grooming"} 621 | c.Quack() 622 | } 623 | 624 | ``` 625 | 626 | 编译上述的代码其实会得到如下所示的汇编指令,需要注意的是为了代码更容易理解和分析,这里的汇编指令依然经过了删减,不过不会影响具体的执行过程: 627 | 628 | ``` 629 | XORPS X0, X0 630 | MOVUPS X0, ""..autotmp_1+32(SP) 631 | LEAQ go.string."grooming"(SB), AX 632 | MOVQ AX, ""..autotmp_1+32(SP) 633 | MOVQ $8, ""..autotmp_1+40(SP) 634 | LEAQ go.itab."".Cat,"".Duck(SB), AX 635 | MOVQ AX, (SP) 636 | LEAQ ""..autotmp_1+32(SP), AX 637 | MOVQ AX, 8(SP) 638 | CALL runtime.convT2I(SB) 639 | MOVQ 16(SP), AX 640 | MOVQ 24(SP), CX 641 | MOVQ 24(AX), AX 642 | MOVQ CX, (SP) 643 | CALL AX 644 | 645 | ``` 646 | 647 | 我们先来看一下上述汇编代码中用于初始化 Cat 结构体的部分: 648 | 649 | ``` 650 | XORPS X0, X0 ;; X0 = 0 651 | MOVUPS X0, ""..autotmp_1+32(SP) ;; StringHeader(SP+32).Data = 0 652 | LEAQ go.string."grooming"(SB), AX ;; AX = &"grooming" 653 | MOVQ AX, ""..autotmp_1+32(SP) ;; StringHeader(SP+32).Data = AX 654 | MOVQ $8, ""..autotmp_1+40(SP) ;; StringHeader(SP+32).Len =8 655 | 656 | ``` 657 | 658 | 这段汇编指令的工作其实与上一节中的差不多,这里会在栈上占用 16 字节初始化 Cat 结构体,不过而上一节中的代码在堆上申请了 16 字节的内存空间,栈上只是一个指向 Cat 结构体的指针。 659 | 660 | 初始化了结构体就进入了类型转换的阶段,编译器会将 go.itab."".Cat,"".Duck 的地址和指向 Cat 结构体的指针一并传入 runtime.convT2I 函数: 661 | 662 | ``` 663 | LEAQ go.itab."".Cat,"".Duck(SB), AX ;; AX = &(go.itab."".Cat,"".Duck) 664 | MOVQ AX, (SP) ;; SP = AX 665 | LEAQ ""..autotmp_1+32(SP), AX ;; AX = &(SP+32) = &Cat{Name: "grooming"} 666 | MOVQ AX, 8(SP) ;; SP + 8 = AX 667 | CALL runtime.convT2I(SB) ;; runtime.convT2I(SP, SP+8) 668 | 669 | ``` 670 | 671 | 这个函数会获取 itab 中存储的类型,根据类型的大小申请一片内存空间并将 elem 指针中的内容拷贝到目标的内存空间中: 672 | 673 | ``` 674 | func convT2I(tab *itab, elem unsafe.Pointer) (i iface) { 675 | t := tab._type 676 | x := mallocgc(t.size, t, true) 677 | typedmemmove(t, x, elem) 678 | i.tab = tab 679 | i.data = x 680 | return 681 | } 682 | 683 | ``` 684 | 685 | convT2I 在函数的最后会返回一个 iface 结构体,其中包含 itab 指针和拷贝的 Cat 结构体,在当前函数返回值之后,main 函数的栈上就会包含以下的数据: 686 | 687 | ![](img/cat3.png) 688 | 689 | 注意有两个 iface 的 itab 结构,一个位于 SP 上,一个是 convT2I 函数返回的。 690 | 691 | SP 和 SP+8 中存储的 itab 和 Cat 指针就是 runtime.convT2I 函数的入参,这个函数的返回值位于 SP+16,是一个占 16 字节内存空间的 iface 结构体,SP+32 存储的就是在栈上的 Cat 结构体,它会在 runtime.convT2I 执行的过程中被拷贝到堆上。 692 | 693 | 在最后,我们会通过以下的操作调用 Cat 实现的接口方法 Quack(): 694 | 695 | ``` 696 | MOVQ 16(SP), AX ;; AX = &(go.itab."".Cat,"".Duck) 697 | MOVQ 24(SP), CX ;; CX = &Cat{Name: "grooming"} 698 | MOVQ 24(AX), AX ;; AX = AX.fun[0] = Cat.Quack 699 | MOVQ CX, (SP) ;; SP = CX 700 | CALL AX ;; CX.Quack() 701 | 702 | ``` 703 | 704 | 这几个汇编指令中的大多数还是非常好理解的,其中的 MOVQ 24(AX), AX 应该是最重要的指令,它从 itab 结构体中取出 Cat.Quack 方法指针,作为 CALL 指令调用时的参数,第 24 字节是 itab.fun 字段开始的位置,由于 Duck 接口只包含一个方法,所以 itab.fun[0] 中存储的就是指向 Quack 的指针了。 705 | 706 | ### convI2I 707 | 708 | 上面的 convT2I 略微简单,因为 itab 是编译期已经确定的全局符号,因此运行时只需把它赋值给新的 interface 变量即可。 709 | 710 | 但是 convI2I 是 interface 到 interface 的转化,这个就涉及到了 interface 函数的变化。 711 | 712 | ``` 713 | func convI2I(inter *interfacetype, i iface) (r iface) { 714 | tab := i.tab 715 | if tab == nil { 716 | return 717 | } 718 | if tab.inter == inter { 719 | r.tab = tab 720 | r.data = i.data 721 | return 722 | } 723 | r.tab = getitab(inter, tab._type, false) 724 | r.data = i.data 725 | return 726 | } 727 | 728 | ``` 729 | 730 | 函数中 inter 是想要转化成的接口类型,i 是现在变量的接口类型。我们可以见到,函数最关键的是 getitab 函数,它的参数一是想要转化为的接口,参数二是接口中数据的实际类型 _type。 731 | 732 | 可以看到,这个函数 733 | 734 | ``` 735 | func getitab(inter *interfacetype, typ *_type, canfail bool) *itab { 736 | var m *itab 737 | 738 | // First, look in the existing table to see if we can find the itab we need. 739 | // This is by far the most common case, so do it without locks. 740 | // Use atomic to ensure we see any previous writes done by the thread 741 | // that updates the itabTable field (with atomic.Storep in itabAdd). 742 | t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable))) 743 | if m = t.find(inter, typ); m != nil { 744 | goto finish 745 | } 746 | 747 | // Not found. Grab the lock and try again. 748 | lock(&itabLock) 749 | if m = itabTable.find(inter, typ); m != nil { 750 | unlock(&itabLock) 751 | goto finish 752 | } 753 | 754 | // Entry doesn't exist yet. Make a new entry & add it. 755 | m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys)) 756 | m.inter = inter 757 | m._type = typ 758 | m.init() 759 | itabAdd(m) 760 | unlock(&itabLock) 761 | finish: 762 | if m.fun[0] != 0 { 763 | return m 764 | } 765 | if canfail { 766 | return nil 767 | } 768 | // this can only happen if the conversion 769 | // was already done once using the , ok form 770 | // and we have a cached negative result. 771 | // The cached result doesn't record which 772 | // interface function was missing, so initialize 773 | // the itab again to get the missing function name. 774 | panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()}) 775 | } 776 | 777 | ``` 778 | 779 | - 先用t保存全局itabTable的地址,然后使用t.find去查找,这样是为了防止查找过程中,itabTable被替换导致查找错误。 780 | - 如果没找到,那么就会上锁,然后使用itabTable.find去查找,这样是因为在第一步查找的同时,另外一个协程写入,可能导致实际存在却查找不到,这时上锁避免itabTable被替换,然后直接在itaTable中查找。 781 | - 再没找到,说明确实没有,那么就根据接口类型、数据类型,去生成一个新的itab,然后插入到itabTable中,这里可能会导致hash表扩容,如果数据类型并没有实现接口,那么根据调用方式,该报错报错,该panic panic。 782 | 783 | ``` 784 | func (t *itabTableType) find(inter *interfacetype, typ *_type) *itab { 785 | // Implemented using quadratic probing. 786 | // Probe sequence is h(i) = h0 + i*(i+1)/2 mod 2^k. 787 | // We're guaranteed to hit all table entries using this probe sequence. 788 | mask := t.size - 1 789 | h := itabHashFunc(inter, typ) & mask 790 | for i := uintptr(1); ; i++ { 791 | p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize)) 792 | // Use atomic read here so if we see m != nil, we also see 793 | // the initializations of the fields of m. 794 | // m := *p 795 | m := (*itab)(atomic.Loadp(unsafe.Pointer(p))) 796 | if m == nil { 797 | return nil 798 | } 799 | if m.inter == inter && m._type == typ { 800 | return m 801 | } 802 | h += i 803 | h &= mask 804 | } 805 | } 806 | 807 | ``` 808 | 809 | 从注释我们可以看到,golang使用的开放地址探测法,用的是公式h(i) = h0 + i*(i+1)/2 mod 2^k,h0是根据接口类型和数据类型的hash字段算出来的。 810 | 811 | #### itab.init 812 | 813 | 如果实在找不到,那么就要生成一个新的 itab 了: 814 | 815 | 816 | ``` 817 | func (m *itab) init() string { 818 | inter := m.inter 819 | typ := m._type 820 | x := typ.uncommon() 821 | 822 | // both inter and typ have method sorted by name, 823 | // and interface names are unique, 824 | // so can iterate over both in lock step; 825 | // the loop is O(ni+nt) not O(ni*nt). 826 | ni := len(inter.mhdr) 827 | nt := int(x.mcount) 828 | xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt] 829 | j := 0 830 | methods := (*[1 << 16]unsafe.Pointer)(unsafe.Pointer(&m.fun[0]))[:ni:ni] 831 | var fun0 unsafe.Pointer 832 | imethods: 833 | for k := 0; k < ni; k++ { 834 | i := &inter.mhdr[k] 835 | itype := inter.typ.typeOff(i.ityp) 836 | name := inter.typ.nameOff(i.name) 837 | iname := name.name() 838 | ipkg := name.pkgPath() 839 | if ipkg == "" { 840 | ipkg = inter.pkgpath.name() 841 | } 842 | for ; j < nt; j++ { 843 | t := &xmhdr[j] 844 | tname := typ.nameOff(t.name) 845 | if typ.typeOff(t.mtyp) == itype && tname.name() == iname { 846 | pkgPath := tname.pkgPath() 847 | if pkgPath == "" { 848 | pkgPath = typ.nameOff(x.pkgpath).name() 849 | } 850 | if tname.isExported() || pkgPath == ipkg { 851 | if m != nil { 852 | ifn := typ.textOff(t.ifn) 853 | if k == 0 { 854 | fun0 = ifn // we'll set m.fun[0] at the end 855 | } else { 856 | methods[k] = ifn 857 | } 858 | } 859 | continue imethods 860 | } 861 | } 862 | } 863 | // didn't find method 864 | m.fun[0] = 0 865 | return iname 866 | } 867 | m.fun[0] = uintptr(fun0) 868 | m.hash = typ.hash 869 | return "" 870 | } 871 | 872 | ``` 873 | 874 | 从这个方法可以看出来,任何类型的函数都是存放在 typ.uncommon 中的,距离 typ.uncommon 的 x.moff 的位置就是该类型的函数列表。 875 | 876 | 这个方法会检查interface和type的方法是否匹配,即type有没有实现interface。假如interface有n中方法,type有m中方法,那么匹配的时间复杂度是O(n x m),由于interface、type的方法都按字典序排,所以O(n+m)的时间复杂度可以匹配完。在检测的过程中,匹配上了,依次往fun字段写入type中对应方法的地址。如果有一个方法没有匹配上,那么就设置fun[0]为0,在外层调用会检查fun[0]==0,即type并没有实现interface。 877 | 878 | #### itabAdd 879 | 880 | ``` 881 | func itabAdd(m *itab) { 882 | // Bugs can lead to calling this while mallocing is set, 883 | // typically because this is called while panicing. 884 | // Crash reliably, rather than only when we need to grow 885 | // the hash table. 886 | if getg().m.mallocing != 0 { 887 | throw("malloc deadlock") 888 | } 889 | 890 | t := itabTable 891 | if t.count >= 3*(t.size/4) { // 75% load factor 892 | // Grow hash table. 893 | // t2 = new(itabTableType) + some additional entries 894 | // We lie and tell malloc we want pointer-free memory because 895 | // all the pointed-to values are not in the heap. 896 | t2 := (*itabTableType)(mallocgc((2+2*t.size)*sys.PtrSize, nil, true)) 897 | t2.size = t.size * 2 898 | 899 | // Copy over entries. 900 | // Note: while copying, other threads may look for an itab and 901 | // fail to find it. That's ok, they will then try to get the itab lock 902 | // and as a consequence wait until this copying is complete. 903 | iterate_itabs(t2.add) 904 | if t2.count != t.count { 905 | throw("mismatched count during itab table copy") 906 | } 907 | // Publish new hash table. Use an atomic write: see comment in getitab. 908 | atomicstorep(unsafe.Pointer(&itabTable), unsafe.Pointer(t2)) 909 | // Adopt the new table as our own. 910 | t = itabTable 911 | // Note: the old table can be GC'ed here. 912 | } 913 | t.add(m) 914 | } 915 | 916 | ``` 917 | 918 | 可以看到,当hash表使用达到75%或以上时,就会进行扩容,容量是原来的2倍,申请完空间,就会把老表中的数据插入到新的hash表中。然后使itabTable指向新的表,最后把新的itab插入到新表中。 919 | 920 | ``` 921 | func (t *itabTableType) add(m *itab) { 922 | // See comment in find about the probe sequence. 923 | // Insert new itab in the first empty spot in the probe sequence. 924 | mask := t.size - 1 925 | h := itabHashFunc(m.inter, m._type) & mask 926 | for i := uintptr(1); ; i++ { 927 | p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize)) 928 | m2 := *p 929 | if m2 == m { 930 | // A given itab may be used in more than one module 931 | // and thanks to the way global symbol resolution works, the 932 | // pointed-to itab may already have been inserted into the 933 | // global 'hash'. 934 | return 935 | } 936 | if m2 == nil { 937 | // Use atomic write here so if a reader sees m, it also 938 | // sees the correctly initialized fields of m. 939 | // NoWB is ok because m is not in heap memory. 940 | // *p = m 941 | atomic.StorepNoWB(unsafe.Pointer(p), unsafe.Pointer(m)) 942 | t.count++ 943 | return 944 | } 945 | h += i 946 | h &= mask 947 | } 948 | } 949 | 950 | ``` 951 | 952 | ## 类型断言 953 | 954 | ### 类型与接口断言 955 | 956 | ``` 957 | package main 958 | 959 | type Duck interface { 960 | Quack() 961 | } 962 | 963 | type Cat struct { 964 | Name string 965 | } 966 | 967 | //go:noinline 968 | func (c *Cat) Quack() { 969 | println(c.Name + " meow") 970 | } 971 | 972 | func main() { 973 | var c Duck = &Cat{Name: "grooming"} 974 | switch c.(type) { 975 | case *Cat: 976 | cat := c.(*Cat) 977 | cat.Quack() 978 | } 979 | } 980 | 981 | ``` 982 | 983 | 当我们编译了上述代码之后,会得到如下所示的汇编指令,这里截取了从创建结构体到执行 switch/case 结构的代码片段: 984 | 985 | ``` 986 | 00000 TEXT "".main(SB), ABIInternal, $32-0 987 | ... 988 | 00029 XORPS X0, X0 989 | 00032 MOVUPS X0, ""..autotmp_4+8(SP) 990 | 00037 LEAQ go.string."grooming"(SB), AX 991 | 00044 MOVQ AX, ""..autotmp_4+8(SP) 992 | 00049 MOVQ $8, ""..autotmp_4+16(SP) 993 | 00058 CMPL go.itab.*"".Cat,"".Duck+16(SB), $593696792 994 | 00068 JEQ 80 995 | 00070 MOVQ 24(SP), BP 996 | 00075 ADDQ $32, SP 997 | 00079 RET 998 | 00080 LEAQ ""..autotmp_4+8(SP), AX 999 | 00085 MOVQ AX, (SP) 1000 | 00089 CALL "".(*Cat).Quack(SB) 1001 | 00094 JMP 70 1002 | 1003 | ``` 1004 | 我们可以直接跳过初始化 Duck 变量的过程,从 0058 开始分析随后的汇编指令,需要注意的是 SP+8 ~ SP+24 16 个字节的位置存储了 Cat 结构体,Go 语言的编译器做了一些优化,所以我们没有看到 iface 结构体的构建过程,但是对于这里要介绍的类型断言和转换其实没有太多的影响: 1005 | 1006 | ``` 1007 | 00058 CMPL go.itab.*"".Cat,"".Duck+16(SB), $593696792 1008 | ;; if (c.tab.hash != 593696792) { 1009 | 00068 JEQ 80 ;; 1010 | 00070 MOVQ 24(SP), BP ;; BP = SP+24 1011 | 00075 ADDQ $32, SP ;; SP += 32 1012 | 00079 RET ;; return 1013 | ;; } else { 1014 | 00080 LEAQ ""..autotmp_4+8(SP), AX ;; AX = &Cat{Name: "grooming"} 1015 | 00085 MOVQ AX, (SP) ;; SP = AX 1016 | 00089 CALL "".(*Cat).Quack(SB) ;; SP.Quack() 1017 | 00094 JMP 70 ;; ... 1018 | ;; BP = SP+24 1019 | ;; SP += 32 1020 | ;; return 1021 | ;; } 1022 | 1023 | ``` 1024 | 1025 | switch/case 语句生成的汇编指令会将目标类型的 hash 与接口变量中的 itab.hash 进行比较,如果两者完全相等就会认为接口变量的具体类型是 Cat,这时就会进入 0080 所在的分支,开始类型转换的过程,我们会获取 SP+8 存储的 Cat 结构体指针、将其拷贝到 SP 上、调用 Quack 方法,最终恢复当前函数的堆栈后返回,不过如果接口中存在的具体类型不是 Cat,就会直接恢复栈指针并返回到调用方。 1026 | 1027 | ### 接口与接口断言 1028 | 1029 | ``` 1030 | func assertI2I(inter *interfacetype, i iface) (r iface) { 1031 | tab := i.tab 1032 | if tab == nil { 1033 | // explicit conversions require non-nil interface value. 1034 | panic(&TypeAssertionError{nil, nil, &inter.typ, ""}) 1035 | } 1036 | if tab.inter == inter { 1037 | r.tab = tab 1038 | r.data = i.data 1039 | return 1040 | } 1041 | r.tab = getitab(inter, tab._type, false) 1042 | r.data = i.data 1043 | return 1044 | } 1045 | 1046 | func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) { 1047 | tab := i.tab 1048 | if tab == nil { 1049 | return 1050 | } 1051 | if tab.inter != inter { 1052 | tab = getitab(inter, tab._type, true) 1053 | if tab == nil { 1054 | return 1055 | } 1056 | } 1057 | r.tab = tab 1058 | r.data = i.data 1059 | b = true 1060 | return 1061 | } 1062 | 1063 | func assertE2I(inter *interfacetype, e eface) (r iface) { 1064 | t := e._type 1065 | if t == nil { 1066 | // explicit conversions require non-nil interface value. 1067 | panic(&TypeAssertionError{nil, nil, &inter.typ, ""}) 1068 | } 1069 | r.tab = getitab(inter, t, false) 1070 | r.data = e.data 1071 | return 1072 | } 1073 | 1074 | func assertE2I2(inter *interfacetype, e eface) (r iface, b bool) { 1075 | t := e._type 1076 | if t == nil { 1077 | return 1078 | } 1079 | tab := getitab(inter, t, true) 1080 | if tab == nil { 1081 | return 1082 | } 1083 | r.tab = tab 1084 | r.data = e.data 1085 | b = true 1086 | return 1087 | } 1088 | 1089 | ``` 1090 | -------------------------------------------------------------------------------- /Go panic 和 recover.md: -------------------------------------------------------------------------------- 1 | # Go panic 和 recover 2 | 3 | ## 概述 4 | 5 | 在具体介绍和分析 Go 语言中的 panic 和 recover 的实现原理之前,我们首先需要对它们有一些基本的了解;panic 和 recover 两个关键字其实都是 Go 语言中的内置函数,panic 能够改变程序的控制流,当一个函数调用执行 panic 时,它会立刻停止执行函数中其他的代码,而是会运行其中的 defer 函数,执行成功后会返回到调用方。 6 | 7 | 对于上层调用方来说,调用导致 panic 的函数其实与直接调用 panic 类似,所以也会执行所有的 defer 函数并返回到它的调用方,这个过程会一直进行直到当前 Goroutine 的调用栈中不包含任何的函数,这时整个程序才会崩溃,这个『恐慌过程』不仅会被显式的调用触发,还会由于运行期间发生错误而触发。 8 | 9 | 然而 panic 导致的『恐慌』状态其实可以被 defer 中的 recover 中止,recover 是一个只在 defer 中能够发挥作用的函数,在正常的控制流程中,调用 recover 会直接返回 nil 并且没有任何的作用,但是如果当前的 Goroutine 发生了『恐慌』,recover 其实就能够捕获到 panic 抛出的错误并阻止『恐慌』的继续传播。 10 | 11 | ``` 12 | func main() { 13 | defer println("in main") 14 | go func() { 15 | defer println("in goroutine") 16 | panic("") 17 | }() 18 | 19 | println("in main...") 20 | 21 | time.Sleep(1 * time.Second) 22 | } 23 | 24 | // in main... 25 | // in goroutine 26 | // panic: 27 | // ... 28 | 29 | ``` 30 | 31 | 当我们运行这段代码时,其实会发现 main 函数中的 defer 语句并没有执行,执行的其实只有 Goroutine 中的 defer,这其实就印证了 Go 语言在发生 panic 时只会执行当前协程中的 defer 函数,这一点从 上一节 的源代码中也有所体现。 32 | 33 | 另一个例子就不止涉及 panic 和 defer 关键字了,我们可以看一下 recover 是如何让当前函数重新『走向正轨』的: 34 | 35 | ``` 36 | func main() { 37 | defer println("in main") 38 | go func() { 39 | defer println("in goroutine") 40 | defer func() { 41 | if err := recover(); err != nil { 42 | fmt.Println(err) 43 | } 44 | }() 45 | panic("G panic") 46 | }() 47 | 48 | println("in main...") 49 | 50 | time.Sleep(1 * time.Second) 51 | } 52 | 53 | in main... 54 | G panic 55 | in goroutine 56 | in main 57 | 58 | ``` 59 | 60 | 从这个例子中我们可以看到,recover 函数其实只是阻止了当前程序的崩溃,但是当前控制流中的其他 defer 函数还会正常执行。 61 | 62 | ## 实现原理 63 | 64 | ### 数据结构 65 | 66 | panic 在 Golang 中其实是由一个数据结构表示的,每当我们调用一次 panic 函数都会创建一个如下所示的数据结构存储相关的信息: 67 | 68 | ``` 69 | type _panic struct { 70 | argp unsafe.Pointer 71 | arg interface{} 72 | link *_panic 73 | recovered bool 74 | aborted bool 75 | } 76 | ``` 77 | 78 | - argp 是指向 defer 调用时参数的指针; 79 | - arg 是调用 panic 时传入的参数; 80 | - link 指向了更早调用的 _panic 结构; 81 | - recovered 表示当前 _panic 是否被 recover 恢复; 82 | - aborted 表示当前的 panic 是否被强行终止; 83 | 84 | 从数据结构中的 link 字段我们就可以推测出以下的结论 — panic 函数可以被连续多次调用,它们之间通过 link 的关联形成一个链表。 85 | 86 | ### 崩溃 87 | 88 | 首先了解一下没有被 recover 的 panic 函数是如何终止整个程序的,我们来看一下 gopanic 函数的实现 89 | 90 | ``` 91 | func gopanic(e interface{}) { 92 | gp := getg() 93 | // ... 94 | var p _panic 95 | p.arg = e 96 | p.link = gp._panic 97 | gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) 98 | 99 | for { 100 | d := gp._defer 101 | if d == nil { 102 | break 103 | } 104 | 105 | d._panic = (*_panic)(noescape(unsafe.Pointer(&p))) 106 | 107 | p.argp = unsafe.Pointer(getargp(0)) 108 | reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) 109 | p.argp = nil 110 | 111 | d._panic = nil 112 | d.fn = nil 113 | gp._defer = d.link 114 | 115 | pc := d.pc 116 | sp := unsafe.Pointer(d.sp) 117 | freedefer(d) 118 | if p.recovered { 119 | // ... 120 | } 121 | } 122 | 123 | fatalpanic(gp._panic) 124 | *(*int)(nil) = 0 125 | } 126 | 127 | ``` 128 | 129 | 我们暂时省略了 recover 相关的代码,省略后的 gopanic 函数执行过程包含以下几个步骤: 130 | 131 | - 获取当前 panic 调用所在的 Goroutine 协程; 132 | - 创建并初始化一个 _panic 结构体; 133 | - 从当前 Goroutine 中的链表获取一个 _defer 结构体; 134 | - 如果当前 _defer 存在,调用 reflectcall 执行 _defer 中的代码; 135 | - 将下一位的 _defer 结构设置到 Goroutine 上并回到 3; 136 | - 调用 fatalpanic 中止整个程序; 137 | 138 | fatalpanic 函数在中止整个程序之前可能就会通过 printpanics 打印出全部的 panic 消息以及调用时传入的参数: 139 | 140 | ``` 141 | func fatalpanic(msgs *_panic) { 142 | pc := getcallerpc() 143 | sp := getcallersp() 144 | gp := getg() 145 | var docrash bool 146 | systemstack(func() { 147 | if startpanic_m() && msgs != nil { 148 | atomic.Xadd(&runningPanicDefers, -1) 149 | 150 | printpanics(msgs) 151 | } 152 | docrash = dopanic_m(gp, pc, sp) 153 | }) 154 | 155 | if docrash { 156 | crash() 157 | } 158 | 159 | systemstack(func() { 160 | exit(2) 161 | }) 162 | 163 | *(*int)(nil) = 0 // not reached 164 | } 165 | 166 | ``` 167 | 168 | 在 fatalpanic 函数的最后会通过 exit 退出当前程序并返回错误码 2,不同的操作系统其实对 exit 函数有着不同的实现,其实最终都执行了 exit 系统调用来退出程序。 169 | 170 | ### 恢复 171 | 172 | 到了这里我们已经掌握了 panic 退出程序的过程,但是一个 panic 的程序也可能会被 defer 中的关键字 recover 恢复,在这时我们就回到 recover 关键字对应函数 gorecover 的实现了: 173 | 174 | ``` 175 | func gorecover(argp uintptr) interface{} { 176 | p := gp._panic 177 | if p != nil && !p.recovered && argp == uintptr(p.argp) { 178 | p.recovered = true 179 | return p.arg 180 | } 181 | return nil 182 | } 183 | 184 | ``` 185 | 186 | 这个函数的实现其实非常简单,它其实就是会修改 panic 结构体的 recovered 字段,当前函数的调用其实都发生在 gopanic 期间,我们重新回顾一下这段方法的实现: 187 | 188 | ``` 189 | func gopanic(e interface{}) { 190 | // ... 191 | 192 | for { 193 | // reflectcall 194 | 195 | pc := d.pc 196 | sp := unsafe.Pointer(d.sp) 197 | 198 | // ... 199 | if p.recovered { 200 | gp._panic = p.link 201 | for gp._panic != nil && gp._panic.aborted { 202 | gp._panic = gp._panic.link 203 | } 204 | if gp._panic == nil { 205 | gp.sig = 0 206 | } 207 | gp.sigcode0 = uintptr(sp) 208 | gp.sigcode1 = pc 209 | mcall(recovery) 210 | throw("recovery failed") 211 | } 212 | } 213 | 214 | fatalpanic(gp._panic) 215 | *(*int)(nil) = 0 216 | } 217 | 218 | ``` 219 | 220 | 上述这段代码其实从 _defer 结构体中取出了程序计数器 pc 和栈指针 sp 并调用 recovery 方法进行调度,调度之前会准备好 sp、pc 以及函数的返回值: 221 | 222 | ``` 223 | func recovery(gp *g) { 224 | sp := gp.sigcode0 225 | pc := gp.sigcode1 226 | 227 | gp.sched.sp = sp 228 | gp.sched.pc = pc 229 | gp.sched.lr = 0 230 | gp.sched.ret = 1 231 | gogo(&gp.sched) 232 | } 233 | 234 | ``` 235 | 236 | 这里的调度其实会将 deferproc 函数的返回值设置成 1,在这时编译器生成的代码就会帮助我们直接跳转到调用方函数 return 之前并进入 deferreturn 的执行过程. 237 | 238 | 跳转到 deferreturn 函数之后,程序其实就从 panic 的过程中跳出来恢复了正常的执行逻辑,而 gorecover 函数也从 _panic 结构体中取出了调用 panic 时传入的 arg 参数。 -------------------------------------------------------------------------------- /Go 内存一致性模型.md: -------------------------------------------------------------------------------- 1 | # Go 内存一致性模型 2 | 3 | [TOC] 4 | 5 | ## MESI 与 Cache Coherence 6 | 7 | ### CPU 的多级缓存 8 | 9 | 计算机硬件的一些延迟。主要关注两个,L1 cache,0.5ns;内存,100ns。可见,平时我们认为的很快的内存,其实在CPU面前,还是非常慢的。想想一下,执行一条加法指令只要一个周期,但是我们这个加法的执行结果写到内存,却要等100个周期。这样的速度显然无法接受。 10 | 11 | 因此,我们有了Cache,并且是多级的Cache,现在的Intel CPU通常有3级cache,例如我自己的电脑上,L1 data cache 有32K,L1 instruction cache 是32K,L2和L3分别是256K和6144K。不同的架构中,Cache会有所区别,比如超线程的CPU中,L1Cache是独占的,L2是Core共享的。 12 | 13 | anyway,cache其实缓解了内存访问的延迟问题。不过它也带来了另一个问题:一致性。 14 | 15 | 一个变量(一个内存位置)其实可以被多个Cache所共享。那么,当我们需要修改这个变量的时候,Cache要如何保持一致呢? 16 | 17 | 理想情况下,原子地修改多个Cache,但多个CPU之间往往通过总线进行通信,不可能同时修改多个;所以其实要制造一种假象,看起来是原子地修改多个Cache,也就是让Cache看起来是强一致的。 18 | 19 | ### Cache Coherence——MESI 20 | 21 | 基于总线通信去实现Cache的强一致,这个问题比较明确,目前用的比较多的应该是MESI协议,或者是一些优化的协议。基本思想是这样子的:一个Cache加载一个变量的时候,是Exclusive状态,当这个变量被第二个Cache加载,更改状态为Shared;这时候一个CPU要修改变量, 就把状态改为Modified,并且Invalidate其他的Cache,其他的Cache再去读这个变量,达到一致。MESI协议大致是这样子,但是状态转换要比这个复杂的多。 22 | 23 | 缓存行有4种不同的状态: 24 | 25 | - 独占 Exclusive (E):缓存行只在当前缓存中,但是干净的(clean)--缓存数据同于主存数据。当别的缓存读取它时,状态变为共享;当前写数据时,变为已修改状态。 26 | - 共享 Shared (S):缓存行也存在于其它缓存中且是干净的。缓存行可以在任意时刻抛弃。 27 | 无效Invalid (I) 28 | - 已修改 Modified (M):缓存行是脏的(dirty),与主存的值不同。如果别的CPU内核要读主存这块数据,该缓存行必须回写到主存,状态变为共享(S). 29 | - 缓存行是无效的 30 | 31 | ### MESI 的状态转移 32 | 33 | 处理器对缓存的请求,也就是 CPU 与 cache 之间的通讯: 34 | 35 | - PrRd: 处理器请求读一个缓存块 36 | - PrWr: 处理器请求写一个缓存块 37 | 38 | 总线对缓存的请求,也就是 cache 之间的通讯总线: 39 | 40 | - BusRd: 窥探器请求指出其他处理器请求 **读一个** 缓存块 41 | - BusRdX: 窥探器请求指出其他处理器请求 **写一个** 该处理器 **不拥有** 的缓存块 42 | - BusUpgr: 窥探器请求指出其他处理器请求 **写一个** 该处理器 **拥有** 的缓存块 43 | - Flush: 窥探器请求指出请求回写整个缓存到主存 44 | - FlushOpt: 窥探器请求指出整个缓存块被发到总线以发送给另外一个处理器(缓存到缓存的复制) 45 | 46 | ![](img/mesi.png) 47 | 48 | ![](img/mesi2.png) 49 | 50 | 操作仅在缓存行是已修改或独占状态时可自由执行。如果在共享状态,其他缓存都要先把该缓存行置为无效,这种广播操作称作Request For Ownership (RFO). 51 | 52 | 个人认为,图二中缺少 shared 状态下接受到 BusUpgr 的情况,这类情况和 BusRdx 其实是一致的,都是要转化为 Invalid 状态。 53 | 54 | ### MOESI、MESIF、RMW 与 LOCK 前缀指令 55 | 56 | MESI 还有很多扩展协议。 57 | 58 | 常见的扩展包括“O”(Owned)状态,它和 E 状态类似,也是保证缓存间一致性的手段,但它直接共享脏段的内容,而不需要先把它们回写到内存中(“脏段共享”),由此产生了 MOSEI 协议。 59 | 60 | MESIF 是指当多个处理器同时拥有某个 S 状态的缓存段的时候,只有被指定的那个处理器(对应的缓存段为 R 或 F 状态)才能对读操作做出回应,而不是每个处理器都能这么做。这种设计可以降低总线的数据流量。 61 | 62 | #### RMW 63 | 64 | 但是我们注意到一个问题,那就是当我们的 CPU0 cache 处于 Invalid(I) 的时候,我们想要执行 PrWr 的操作。按照协议我们会发出 BusRdX 信号,其他 CPU 会无效它们的副本。那么假如正好有一个 CPU1 的 cache 的状态是 Modified,会发生什么? 65 | 66 | 按照协议,CPU1 会回写主存,并且转化为 Invalid 状态。CPU0 读到 CPU1 发来的新的内存值,然后更改为自己的新值。 67 | 68 | 我们发现,CPU1 缓存的值被 CPU0 覆盖了。 69 | 70 | 对于 Read-Modify-Write 类型的操作影响比较大,例如两个线程都执行 i++。假如 i 的初值为 0,当 RMW 执行 Read 操作的时候,CPU0 cache 还是 Shared 状态,等到 CPU 修改了寄存器,寄存器需要写入到 cache 的时候,CPU1 已经完成写入操作,CPU0 cache 状态已经变成了 Invalid,那么这个时候 CPU0 的 i 值 1 会覆盖掉 CPU1 的自增结果,导致两个 i++ 操作之后,结果还是 1。 71 | 72 | 例如,在 Load-Store 体系中,如果对一个非原子的内存中的变量a加1,则在Load-Store体系中,可能需要: 73 | 74 | ``` 75 | lw r1, a 76 | addi r1, r1, 1 77 | sw a, r1 78 | 79 | ``` 80 | 当一个core执行这段代码的时候,另一个core也可能在执行相同的代码。导致尽管两个core分别对a加了1,最终存回到memory中的a仍然只加了1,而没有加2.虽然任何对齐于数据结构本身的 load 和 store 一般都是原子操作,因为 core 对于这种数据结构的 load 和 store 仅需要一条指令就可以完成,其他 core 没有机会观察到中间状态。但是这三个指令结合起来却不是原子的。 81 | 82 | 对于非 Load-Store 体系,例如 X86, 上面三个指令可能只需要一条指令就可以完成,但是这一条指令实际上 core 还是需要执行载入-更改-写回三步,任何一步都可能被打断。 83 | 84 | 在单处理器系统(UniProcessor,简称 UP)中,能够在单条指令中完成的操作都可以认为是原子操作,因为单核情况下,并发只能出现在中断上下文中,但是中断只能发生在指令与指令之间。 85 | 86 | 在多处理器系统(Symmetric Multi-Processor,简称 SMP)中情况有所不同,由于系统中有多个处理器在独立的运行,存在并行的可能,即使在能单条指令中完成的操作也可能受到干扰。 87 | 88 | 这个时候,就需要一种协调各个 CPU 操作的协议,让这个 RMW 成为一个原子操作,操作期间不会受多核 CPU 的影响。 89 | 90 | #### LOCK 前缀 91 | 92 | 在所有的 X86 CPU 上都具有锁定一个特定内存地址的能力,当这个特定内存地址被锁定后,它就可以阻止其他的系统总线读取或修改这个内存地址。这种能力是通过 LOCK 指令前缀再加上下面的汇编指令来实现的。当使用 LOCK 指令前缀时,它会使 CPU 宣告一个 LOCK# 信号,这样就能确保在多处理器系统或多线程竞争的环境下互斥地使用这个内存地址。当指令执行完毕,这个锁定动作也就会消失。 93 | 94 | 能够和 LOCK 指令前缀一起使用的指令如下所示: 95 | 96 | > BT, BTS, BTR, BTC (mem, reg/imm) 97 | > 98 | > XCHG, XADD (reg, mem / mem, reg) 99 | > 100 | > ADD, OR, ADC, SBB (mem, reg/imm) 101 | > 102 | > AND, SUB, XOR (mem, reg/imm) 103 | > 104 | > NOT, NEG, INC, DEC (mem) 105 | > 106 | 107 | 注意:XCHG 和 XADD (以及所有以 'X' 开头的指令)都能够保证在多处理器系统下的原子操作,它们总会宣告一个 "LOCK#" 信号,而不管有没有 LOCK 前缀。 108 | 109 | 从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。 110 | 111 | 假设两个core都持有相同地址对应cacheline,且各自cacheline 状态为S, 这时如果要想执行 LOCK 指令,成功修改内存值,就首先需要把S转为E或者M, 则需要向其它core invalidate 这个地址的cacheline,则两个core都会向ring bus 发出 invalidate这个操作, 那么在ringbus上就会根据特定的设计协议仲裁是core0,还是core1能赢得这个invalidate, 胜者完成操作, 失败者需要接受结果, invalidate自己对应的cacheline,再读取胜者修改后的值, 回到起点. 112 | 113 | 除此之外,LOCK 还有禁止该指令与之前和之后的读和写指令重排序,把写缓冲区中的所有数据刷新到内存中的功能,这两个功能我们接下来详细再说。 114 | 115 | ### false sharing / true sharing 116 | 117 | #### true sharing 118 | 119 | true sharing 的概念比较好理解,在对全局变量或局部变量进行多线程修改时,就是一种形式的共享,而且非常字面意思,就是 true sharing。true sharing 带来的明显的问题,例如 RWMutex scales poorly 的官方 issue,即 RWMutex 的 RLock 会对 RWMutex 这个对象的 readerCount 原子加一。本质上就是一种 true sharing。 120 | 121 | #### false sharing 122 | 123 | 缓存系统中是以缓存行(cache line)为单位存储的。缓存行通常是 64 字节(译注:本文基于 64 字节,其他长度的如 32 字节等不适本文讨论的重点),并且它有效地引用主内存中的一块地址。一个 Java 的 long 类型是 8 字节,因此在一个缓存行中可以存 8 个 long 类型的变量。所以,如果你访问一个 long 数组,当数组中的一个值被加载到缓存中,它会额外加载另外 7 个,以致你能非常快地遍历这个数组。事实上,你可以非常快速的遍历在连续的内存块中分配的任意数据结构。而如果你在数据结构中的项在内存中不是彼此相邻的(如链表),你将得不到免费缓存加载所带来的优势,并且在这些数据结构中的每一个项都可能会出现缓存未命中。 124 | 125 | 如果存在这样的场景,有多个线程操作不同的成员变量,但是相同的缓存行,这个时候会发生什么?。没错,伪共享(False Sharing)问题就发生了!有张 Disruptor 项目的经典示例图,如下: 126 | 127 | ![](img/falseshare.png) 128 | 129 | 上图中,一个运行在处理器 core1上的线程想要更新变量 X 的值,同时另外一个运行在处理器 core2 上的线程想要更新变量 Y 的值。但是,这两个频繁改动的变量都处于同一条缓存行。两个线程就会轮番发送 RFO 消息,占得此缓存行的拥有权。当 core1 取得了拥有权开始更新 X,则 core2 对应的缓存行需要设为 I 状态。当 core2 取得了拥有权开始更新 Y,则 core1 对应的缓存行需要设为 I 状态(失效态)。轮番夺取拥有权不但带来大量的 RFO 消息,而且如果某个线程需要读此行数据时,L1 和 L2 缓存上都是失效数据,只有 L3 缓存上是同步好的数据。从前一篇我们知道,读 L3 的数据非常影响性能。更坏的情况是跨槽读取,L3 都要 miss,只能从内存上加载。 130 | 131 | 表面上 X 和 Y 都是被独立线程操作的,而且两操作之间也没有任何关系。只不过它们共享了一个缓存行,但所有竞争冲突都是来源于共享。 132 | 133 | 那么该如何做到呢?其实在我们注释的那行代码中就有答案,那就是缓存行填充(Padding) 。现在分析上面的例子,我们知道一条缓存行有 64 字节,而 Java 程序的对象头固定占 8 字节(32位系统)或 12 字节( 64 位系统默认开启压缩, 不开压缩为 16 字节),所以我们只需要填 6 个无用的长整型补上6*8=48字节,让不同的 VolatileLong 对象处于不同的缓存行,就避免了伪共享( 64 位系统超过缓存行的 64 字节也无所谓,只要保证不同线程不操作同一缓存行就可以)。 134 | 135 | 在 Go 的 runtime 中有不少例子,特别是那些 per-P 的结构,大多都有针对 false sharing 的优化: 136 | 137 | runtime/time.go 138 | 139 | ``` 140 | var timers [timersLen]struct { 141 | timersBucket 142 | 143 | // The padding should eliminate false sharing 144 | // between timersBucket values. 145 | pad [cpu.CacheLinePadSize - unsafe.Sizeof(timersBucket{})%cpu.CacheLinePadSize]byte 146 | } 147 | 148 | ``` 149 | 150 | runtime/sema.go 151 | 152 | ``` 153 | var semtable [semTabSize]struct { 154 | root semaRoot 155 | pad [cpu.CacheLinePadSize - unsafe.Sizeof(semaRoot{})]byte 156 | } 157 | 158 | ``` 159 | 160 | ## CPU 内存一致性模型 161 | 162 | ### store buffer 163 | 164 | 看起来很美好的MESI协议,其实有一些问题。比如说,修改变量的时候,要发送一些Invalidate给远程的CPU,等到远程CPU返回一个ACK,才能进行下一步。 这一过程中如果远程的CPU比较繁忙,甚至会带来更大的延迟。并且如果有内存访问,会带来几百个周期的延迟。 165 | 166 | 那么有没有优化手段,能够并行访问内存?或者对内存操作乱序执行? 167 | 168 | 这里用了一个称之为store buffer的结构,来对store操作进行优化。就Store操作来说,这结构所带来的效果就是,不需要等到Cache同步到所有CPU之后Store操作才返回,可能是写了本地的Store buffer就返回,什么时候所有 CPU 的 Invalidate 消息返回了再异步写进 cache line。显然,这个结果对于延迟的优化是十分明显的。 169 | 170 | 因而,无论什么时候 CPU 需要从 cache line 中读取,都需要先扫描它自己的 store buffer 来确认是否存在相同的 line,因为有可能当前 CPU 在这次操作之前曾经写入过 cache,但该数据还没有被刷入过 cache(之前的写操作还在 store buffer 中等待)。需要注意的是,虽然 CPU 可以读取其之前写入到 store buffer 中的值,但其它 CPU 并不能在该 CPU 将 store buffer 中的内容 flush 到 cache 之前看到这些值。即 store buffer 是不能跨核心访问的,CPU 核心看不到其它核心的 store buffer。 171 | 172 | ### Invalidate Queue 173 | 174 | 为了处理 invalidation 消息,CPU 实现了 invalidate queue,借以处理新达到的 invalidate 请求,在这些请求到达时,可以马上进行响应,但可以不马上处理。取而代之的,invalidation 消息只是会被推进一个 invalidation 队列,并在之后尽快处理(但不是马上)。因此,CPU 可能并不知道在它 cache 里的某个 cache line 是 invalid 状态的,因为 invalidation 队列包含有收到但还没有处理的 invalidation 消息,CPU 在读取数据的时候,并不像 store buffer 那样提取读取 Invalidate Queue。 175 | 176 | ### CPU 内存一致性模型 177 | 178 | 目前有多种内存一致性模型: 179 | 180 | - 顺序存储模型(sequential consistency model) 181 | - 完全存储定序(total store order) 182 | - 部分存储定序(part store order) 183 | - 宽松存储模型(relax memory order) 184 | 185 | 186 | ![](img/mesi3.jpg) 187 | 188 | #### 顺序存储模型 SC 189 | 190 | 在顺序存储器模型里,MP(多核)会严格严格按照代码指令流来执行代码, 所以上面代码在主存里的访问顺序是: 191 | 192 | > S1 S2 L1 L2 193 | 194 | 通过上面的访问顺序我们可以看出来,虽然C1与C2的指令虽然在不同的CORE上运行,但是C1发出来的访问指令是顺序的,同时C2的指令也是顺序的。虽然这两个线程跑在不同的CPU上,但是在顺序存储模型上,其访问行为与UP(单核)上是一致的。 195 | 我们最终看到r2的数据会是NEW,与期望的执行情况是一致的,所以在顺序存储模型上是不会出现内存访问乱序的情况. 196 | 197 | #### 完全存储定序 TSO 198 | 199 | 这里我们之前所说的 store buffer 与 Invalidate Queue 开始登场,首先我们思考单核上的两条指令: 200 | 201 | ``` 202 | S1:store flag= set 203 | S2:load r1=data 204 | S3:store b=set 205 | ``` 206 | 207 | 如果在顺序存储模型中,S1肯定会比S2先执行。但是如果在加入了store buffer之后,S1将指令放到了store buffer后会立刻返回,这个时候会立刻执行S2。S2是read指令,CPU必须等到数据读取到r1后才会继续执行。这样很可能S1的store flag=set指令还在store buffer上,而S2的load指令可能已经执行完(特别是data在cache上存在,而flag没在cache中的时候。这个时候CPU往往会先执行S2,这样可以减少等待时间) 208 | 209 | 这里就可以看出再加入了store buffer之后,内存一致性模型就发生了改变。 210 | 如果我们定义store buffer必须严格按照FIFO的次序将数据发送到主存(所谓的FIFO表示先进入store buffer的指令数据必须先于后面的指令数据写到存储器中),这样S3必须要在S1之后执行,CPU能够保证store指令的存储顺序,这种内存模型就叫做完全存储定序(TSO)。 211 | 212 | ![](img/mesi4.jpg) 213 | 214 | 在SC模型里,C1与C2是严格按照顺序执行的 215 | 代码可能的执行顺序如下: 216 | 217 | ``` 218 | S1 S2 L1 L2 219 | S1 L1 S2 L2 220 | S1 L1 L2 S2 221 | L1 L2 S1 S2 222 | L1 S1 S2 L2 223 | L1 S1 L2 S2 224 | 225 | ``` 226 | 由于SC会严格按照顺序进行,最终我们看到的结果是至少有一个CORE的r1值为NEW,或者都为NEW。 227 | 228 | 在TSO模型里,由于store buffer的存在,L1和S1的store指令会被先放到store buffer里面,然后CPU会继续执行后面的load指令。Store buffer中的数据可能还没有来得及往存储器中写,这个时候我们可能看到C1和C2的r1都为0的情况。 229 | 所以,我们可以看到,在store buffer被引入之后,内存一致性模型已经发生了变化(从SC模型变为了TSO模型),会出现store-load乱序的情况,这就造成了代码执行逻辑与我们预先设想不相同的情况。而且随着内存一致性模型越宽松(通过允许更多形式的乱序读写访问),这种情况会越剧烈,会给多线程编程带来很大的挑战。 230 | 231 | 这个就是所谓的 Store-Load 乱序,x86 唯一的乱序就是这个了。 232 | 233 | #### 部分存储定序 PSO 234 | 235 | 芯片设计人员并不满足TSO带来的性能提升,于是他们在TSO模型的基础上继续放宽内存访问限制,允许CPU以非FIFO来处理store buffer缓冲区中的指令。CPU只保证地址相关指令在store buffer中才会以FIFO的形式进行处理,而其他的则可以乱序处理,所以这被称为部分存储定序(PSO)。 236 | 237 | 那我们继续分析下面的代码 238 | 239 | ![](img/mesi3.jpg) 240 | 241 | S1与S2是地址无关的store指令,cpu执行的时候都会将其推到store buffer中。如果这个时候flag在C1的cahe中存在,那么CPU会优先将S2的store执行完,然后等data缓存到C1的cache之后,再执行store data=NEW指令。 242 | 243 | 这个时候可能的执行顺序: 244 | 245 | ``` 246 | S2 L1 L2 S1 247 | ``` 248 | 249 | 这样在C1将data设置为NEW之前,C2已经执行完,r2最终的结果会为0,而不是我们期望的NEW,这样PSO带来的store-store乱序将会对我们的代码逻辑造成致命影响。 250 | 251 | 从这里可以看到,store-store乱序的时候就会将我们的多线程代码完全击溃。所以在PSO内存模型的架构上编程的时候,要特别注意这些问题。 252 | 253 | #### 宽松内存模型 RMO 254 | 255 | 丧心病狂的芯片研发人员为了榨取更多的性能,在PSO的模型的基础上,更进一步的放宽了内存一致性模型,不仅允许store-load,store-store乱序。还进一步允许load-load,load-store乱序, 只要是地址无关的指令,在读写访问的时候都可以打乱所有load/store的顺序,这就是宽松内存模型(RMO)。 256 | 257 | 我们再看看上面分析过的代码 258 | 259 | ![](img/mesi3.jpg) 260 | 261 | 在PSO模型里,由于S2可能会比S1先执行,从而会导致C2的r2寄存器获取到的data值为0。在RMO模型里,不仅会出现PSO的store-store乱序,C2本身执行指令的时候,由于L1与L2是地址无关的,所以L2可能先比L1执行,这样即使C1没有出现store-store乱序,C2本身的load-load乱序也会导致我们看到的r2为0。从上面的分析可以看出,RMO内存模型里乱序出现的可能性会非常大,这是一种乱序随可见的内存一致性模型。 262 | 263 | 264 | ### 内存屏障 265 | 266 | 芯片设计人员为了尽可能的榨取CPU的性能,引入了乱序的内存一致性模型,这些内存模型在多线程的情况下很可能引起软件逻辑问题。为了解决在有些一致性模型上可能出现的内存访问乱序问题,芯片设计人员提供给了内存屏障指令,用来解决这些问题。 267 | 内存屏障的最根本的作用就是提供一个机制,要求CPU在这个时候必须以顺序存储一致性模型的方式来处理load与store指令,这样才不会出现内存访问不一致的情况。 268 | 269 | 对于TSO和PSO模型,内存屏障只需要在store-load/store-store时需要(写内存屏障),最简单的一种方式就是内存屏障指令必须保证store buffer数据全部被清空的时候才继续往后面执行,这样就能保证其与SC模型的执行顺序一致。 270 | 而对于RMO,在PSO的基础上又引入了load-load与load-store乱序。RMO的读内存屏障就要保证前面的load指令必须先于后面的load/store指令先执行,不允许将其访问提前执行。 271 | 272 | 我们继续看下面的例子: 273 | 274 | ![](img/mesi3.jpg) 275 | 276 | 例如C1执行S1与S2的时候,我们在S1与S2之间加上写屏障指令,要求C1按照顺序存储模型来进行store的执行,而在C2端的L1与L2之间加入读内存屏障,要求C2也按照顺序存储模型来进行load操作,这样就能够实现内存数据的一致性,从而解决乱序的问题。 277 | 278 | #### barrier 279 | 280 | 从上面来看,barrier 有四种: 281 | 282 | - LoadLoad 阻止不相关的 Load 操作发生重排 283 | - LoadStore 阻止 Store 被重排到 Load 之前 284 | - StoreLoad 阻止 Load 被重排到 Store 之前 285 | - StoreStore 阻止 Store 被重排到 Store 之前 286 | 287 | #### sfence/lfence/mfence/lock 288 | 289 | Intel为此提供三种内存屏障指令: 290 | 291 | - sfence ,实现Store Barrior 会将store buffer中缓存的修改刷入L1 cache中,使得其他cpu核可以观察到这些修改,而且之后的写操作不会被调度到之前,即sfence之前的写操作一定在sfence完成且全局可见; 292 | - lfence ,实现Load Barrior 会将invalidate queue失效,强制读取入L1 cache中,而且lfence之后的读操作不会被调度到之前,即lfence之前的读操作一定在lfence完成(并未规定全局可见性); 293 | - mfence ,实现Full Barrior 同时刷新store buffer和invalidate queue,保证了mfence前后的读写操作的顺序,同时要求mfence之后写操作结果全局可见之前,mfence之前写操作结果全局可见; 294 | - lock 用来修饰当前指令操作的内存只能由当前CPU使用,若指令不操作内存仍然由用,因为这个修饰会让指令操作本身原子化,而且自带Full Barrior效果;还有指令比如IO操作的指令、exch等原子交换的指令,任何带有lock前缀的指令以及CPUID等指令都有内存屏障的作用。 295 | 296 | X86-64下仅支持一种指令重排:Store-Load ,即读操作可能会重排到写操作前面,同时不同线程的写操作并没有保证全局可见,要注意的是这个问题只能用mfence解决,不能靠组合sfence和lfence解决。 297 | 298 | 但是这不代表硬件实现上L/S FENCE是什么都不做. LFENCE永远会挡住时间较晚的Load不要提前完成. SFENCE挡住时间较早的Store不要滞后完成。 299 | 300 | #### 其他乱序手段 301 | 302 | 但是,处理器领域其实还有很多的优化手段,流水线执行、乱序执行、预测执行等等,各种我们听过和没听过的优化,这个时候我们就应该使用内存屏障。 303 | 304 | ## 内存模型 与 Memory Consistency 305 | 306 | 内存模型(Memory Model),它是系统和程序员之间的规范,它规定了存储器访问的行为,并影响到了性能。并且,Memory Model有多层,处理器规定、编译器规定、高级语。对于高级语言来说, 它通常需要支持跨平台,也就是说它会基于各种不同的内存模型,但是又要提供给程序员一个统一的内存模型,可以理解为一个适配器的角色。 307 | 308 | ### Acquire 与 Release语义 309 | 310 | 因为 store-load 可以被重排,所以x86不是顺序一致。但是因为其他三种读写顺序不能被重排,所以x86是 acquire/release 语义。 311 | 312 | - 对于Acquire来说,保证Acquire后的读写操作不会发生在Acquire动作之前, 即 load-load, load-store 不能被重排 313 | - 对于Release来说,保证Release前的读写操作不会发生在Release动作之后, 即 load-store, store-store 不能被重排。 314 | 315 | X86-64中Load读操作本身满足Acquire语义,Store写操作本身也是满足Release语义。但Store-Load操作间等于没有保护,因此仍需要靠 mfence 或 lock 等指令才可以满足到Synchronizes-with规则。 316 | 317 | 简单的说,就是 acquire 只禁止后面的代码不能够重排,但是 acquire 语句自己可以向前面走。release 语句只禁止前面的代码不能重排,因此 release 可以向后走。只有 mfence 才能阻止 acquire 与 release 语句的重排。 318 | 319 | ### synchronizes-with 与 happens-before 320 | 321 | ``` 322 | void write_x_then_y() 323 | { 324 | x.store(true,std::memory_order_relaxed); 325 | y.store(true,std::memory_order_release); 326 | } 327 | void read_y_then_x() 328 | { 329 | while(!y.load(std::memory_order_acquire)); 330 | if(x.load(std::memory_order_relaxed)){ 331 | ++z; 332 | } 333 | } 334 | 335 | ``` 336 | 337 | write_x_then_y()中的 y.store(true,std::memory_order_release);与read_y_then_x()的while(!y.load(std::memory_order_acquire));是一种synchronizes-with关系, 338 | 339 | y.store(true,std::memory_order_release);与x.load(std::memory_order_relaxed)是一种happens-before关系。 340 | 341 | ### C++ 六种 memory order 342 | 343 | 对应于 CPU 的四种内存模型,语言层面 C++ 也定义了 6 中内存模型: 344 | 345 | #### Relaxed ordering 346 | 347 | Relaxed ordering: 在单个线程内,所有原子操作是顺序进行的。按照什么顺序?基本上就是代码顺序(sequenced-before)。这就是唯一的限制了!两个来自不同线程的原子操作是什么顺序?两个字:任意。 348 | 349 | #### Release -- acquire 350 | 351 | Release -- acquire: 来自不同线程的两个原子操作顺序不一定?那怎么能限制一下它们的顺序?这就需要两个线程进行一下同步(synchronize-with)。同步什么呢?同步对一个变量的读写操作。线程 A 原子性地把值写入 x (release), 然后线程 B 原子性地读取 x 的值(acquire). 这样线程 B 保证读取到 x 的最新值。注意 release -- acquire 有个副作用:线程 A 中所有发生在 release x 之前的写操作,对在线程 B acquire x 之后的任何读操作都可见!本来 A, B 间读写操作顺序不定。这么一同步,在 x 这个点前后, A, B 线程之间有了个顺序关系,称作 inter-thread happens-before. 352 | 353 | ``` 354 | // write_x_then_y和read_y_then_x各自执行在一个线程中 355 | // x原子变量采用的是relaxed order, y原子变量采用的是acquire-release order 356 | // 两个线程中的y原子存在synchronizes-with的关系,read_y_then_x的load与 357 | // write_x_then_y的y.store存在一种happens-before的关系 358 | // write_x_then_y的y.store执行后能保证read_y_then_x的x.load读到的x一定是true。 359 | // 虽然relaxed并不保证happens-before关系,但是在同一线程里,release会保证在其之前的原子 360 | // store操作都能被看见, acquire能保证通线程中的后续的load都能读到最新指。 361 | // 所以当y.load为true的时候,x肯定可以读到最新值。所以即使这里x用的是relaxed操作,所以其也能 362 | // 达到acquire-release的作用。 363 | // 具体为什么会这样,后续单独讲解 364 | void write_x_then_y() 365 | { 366 | x.store(true,std::memory_order_relaxed); 367 | y.store(true,std::memory_order_release); 368 | } 369 | void read_y_then_x() 370 | { 371 | while(!y.load(std::memory_order_acquire)); 372 | if(x.load(std::memory_order_relaxed)){ 373 | ++z; 374 | } 375 | } 376 | 377 | ``` 378 | 379 | #### Release -- consume 380 | 381 | Release -- consume: 我只想同步一个 x 的读写操作,结果把 release 之前的写操作都顺带同步了?如果我想避免这个额外开销怎么办?用 release -- consume 呗。同步还是一样的同步,这回副作用弱了点:在线程 B acquire x 之后的读操作中,有一些是依赖于 x 的值的读操作。管这些依赖于 x 的读操作叫 赖B读. 同理在线程 A 里面, release x 也有一些它所依赖的其他写操作,这些写操作自然发生在 release x 之前了。管这些写操作叫 赖A写. 现在这个副作用就是,只有 赖B读 能看见 赖A写. 382 | 383 | 什么叫数据依赖(carries dependency) 384 | 385 | ``` 386 | S1. c = a + b; 387 | S2. e = c + d; 388 | 389 | ``` 390 | S2 数据依赖于 S1,因为它需要 c 的值。 391 | 392 | #### Sequential consistency 393 | 394 | Sequential consistency: 理解了前面的几个,顺序一致性就最好理解了。在前面 Release -- acquire 类似于 CPU 的 TSO 模型,它还是允许 Store-Load 这种重排的,但是对于顺序一致性模型,这种重排也是不允许的: 395 | 396 | ``` 397 | x = 0,y = 0; 398 | 399 | void write_x_then_read_y() 400 | { 401 | x.store(true, std::memory_order_relaxed); 402 | 403 | if (! y.load(std::memory_order_acquire)) { 404 | ++z; 405 | }; 406 | } 407 | 408 | void write_y_then_read_x() 409 | { 410 | y.store(true, std::memory_order_release); 411 | 412 | if (! x.load(std::memory_order_relaxed)){ 413 | ++z; 414 | } 415 | } 416 | ``` 417 | 这种代码默认了两个函数只能自增一次 z,不会同时进入 if 条件。但是对于 Store-Load 这种,很可能 store 操作被重排到 load 之后,那么临界区的 if 条件就会失效。 418 | 419 | 还有一种极端情况如下,它可以保证 x 与 y 在所有的线程中,显现的顺序是一致的。也就是说 x 与 y 一定是 (0,0)、(1,0)、(0,1)的一种,不可能在线程1中是 (1,0),而在线程2中是 (0,1), 其他的内存模型无法保证。 420 | 421 | ps: Release -- acquire 的定义的确是不会保证,但是这个也不涉及 Store-load 重排。猜测可能与具体的 Release -- acquire 实现有关,不同的变量 x 与 y 同时被更新,即使没有发生 load-load 的指令重排,传递到各个 CPU 的顺序也会不同。 422 | 423 | ``` 424 | std::atomic x = {false}; 425 | std::atomic y = {false}; 426 | std::atomic z = {0}; 427 | 428 | void write_x() 429 | { 430 | x.store(true, std::memory_order_seq_cst); 431 | } 432 | 433 | void write_y() 434 | { 435 | y.store(true, std::memory_order_seq_cst); 436 | } 437 | 438 | void read_x_then_y() 439 | { 440 | while (!x.load(std::memory_order_seq_cst)) 441 | ; 442 | if (y.load(std::memory_order_seq_cst)) { 443 | ++z; 444 | } 445 | } 446 | 447 | void read_y_then_x() 448 | { 449 | while (!y.load(std::memory_order_seq_cst)) 450 | ; 451 | if (x.load(std::memory_order_seq_cst)) { 452 | ++z; 453 | } 454 | } 455 | 456 | int main() 457 | { 458 | std::thread a(write_x); 459 | std::thread b(write_y); 460 | std::thread c(read_x_then_y); 461 | std::thread d(read_y_then_x); 462 | a.join(); b.join(); c.join(); d.join(); 463 | assert(z.load() != 0); // will never happen 464 | } 465 | 466 | ``` 467 | 468 | ### Volatile 关键字 469 | 470 | #### 易变性 471 | 472 | - 非Volatile变量 473 | 474 | ![](img/volatile.png) 475 | 476 | b = a + 1;这条语句,对应的汇编指令是:lea ecx, [eax + 1]。由于变量a,在前一条语句a = fn(c)执行时,被缓存在了寄存器eax中,因此b = a + 1;语句,可以直接使用仍旧在寄存器eax中的a,来进行计算,对应的也就是汇编:[eax + 1]。 477 | 478 | - Volatile变量 479 | 480 | ![](img/volatile1.png) 481 | 482 | 与测试用例一唯一的不同之处,是变量a被设置为volatile属性,一个小小的变化,带来的是汇编代码上很大的变化。a = fn(c)执行后,寄存器ecx中的a,被写回内存:mov dword ptr [esp+0Ch], ecx。然后,在执行b = a + 1;语句时,变量a有重新被从内存中读取出来:mov eax, dword ptr [esp + 0Ch],而不再直接使用寄存器ecx中的内容。 483 | 484 | #### 不可优化性 485 | 486 | ![](img/volatile3.png) 487 | 488 | 在这个用例中,非volatile变量a,b,c全部被编译器优化掉了 (optimize out),因为编译器通过分析,发觉a,b,c三个变量是无用的,可以进行常量替换。最后的汇编代码相当简介,高效率。 489 | 490 | ![](img/volatile4.png) 491 | 492 | 测试用例四,与测试用例三类似,不同之处在于,a,b,c三个变量,都是volatile变量。这个区别,反映到汇编语言中,就是三个变量仍旧存在,需要将三个变量从内存读入到寄存器之中,然后再调用printf()函数。 493 | 494 | #### 编译顺序性 495 | 496 | 个线程(Thread1)在完成一些操作后,会修改这个变量。而另外一个线程(Thread2),则不断读取这个flag变量,由于flag变量被声明了volatile属性,因此编译器在编译时,并不会每次都从寄存器中读取此变量,同时也不会通过各种激进的优化,直接将if (flag == true)改写为if (false == true)。只要flag变量在Thread1中被修改,Thread2中就会读取到这个变化,进入if条件判断,然后进入if内部进行处理。在if条件的内部,由于flag == true,那么假设Thread1中的something操作一定已经完成了,在基于这个假设的基础上,继续进行下面的other things操作。 497 | 498 | 通过将flag变量声明为volatile属性,很好的利用了本文前面提到的C/C++ Volatile的两个特性:”易变”性;”不可优化”性。按理说,这是一个对于volatile关键词的很好应用,而且看到这里的朋友,也可以去检查检查自己的代码,我相信肯定会有这样的使用存在。 499 | 500 | 但是,这个多线程下看似对于C/C++ Volatile关键词完美的应用,实际上却是有大问题的。问题的关键,就在于前面标红的文字: 501 | 502 | 由于flag = true,那么假设Thread1中的something操作一定已经完成了。flag == true,为什么能够推断出Thread1中的something一定完成了?其实既然我把这作为一个错误的用例,答案是一目了然的:这个推断不能成立,你不能假设看到flag == true后,flag = true;这条语句前面的something一定已经执行完成了。这就引出了C/C++ Volatile关键词的第三个特性:顺序性。 503 | 504 | 简单的说,Volatile 并不具备内存屏障的作用,没有 happens-before 的语义。 505 | 506 | ![](img/volatile5.png) 507 | 508 | 虽然 Volatile 并不具备内存屏障的作用,但是它的确在编译期阻止了 Volatile 变量与 Volatile 的编译乱序,具体到 CPU 的内存模型上,是否还会乱序那就不是 Volatile 关键字可以控制的了。 509 | 510 | ### Volatile:Java增强 511 | 512 | 与C/C++的Volatile关键词类似,Java的Volatile也有这三个特性,但最大的不同在于:第三个特性,”顺序性”,Java的Volatile有很极大的增强,Java Volatile变量的操作,附带了Acquire与Release语义。 513 | 514 | - 对于Java Volatile变量的写操作,带有Release语义,所有Volatile变量写操作之前的针对其他任何变量的读写操作,都不会被编译器、CPU优化后,乱序到Volatile变量的写操作之后执行。 515 | 516 | - 对于Java Volatile变量的读操作,带有Acquire语义,所有Volatile变量读操作之后的针对其他任何变量的读写操作,都不会被编译器、CPU优化后,乱序到Volatile变量的读操作之前进行。 517 | 518 | ## Golang 内存模型 519 | 520 | ### 同步 521 | 522 | #### 初始化 523 | 524 | 如果在一个goroutine所在的源码包p里面通过import命令导入了包q,那么q包里面go文件的初始化方法的执行会happens before 于包p里面的初始化方法执行 525 | 526 | #### 创建goroutine 527 | 528 | go语句启动一个新的goroutine的动作 happen before 该新goroutine的运行 529 | 530 | ``` 531 | package main 532 | 533 | import ( 534 | "fmt" 535 | "sync" 536 | ) 537 | 538 | var a string 539 | var wg sync.WaitGroup 540 | 541 | func f() { 542 | fmt.Print(a) 543 | wg.Done() 544 | } 545 | 546 | func hello() { 547 | a = "hello, world" 548 | go f() 549 | } 550 | func main() { 551 | wg.Add(1) 552 | 553 | hello() 554 | wg.Wait() 555 | 556 | } 557 | ``` 558 | 559 | 如上代码调用hello方法后肯定会输出"hello,world",可能等hello方法执行完毕后才输出(由于调度的原因)。 560 | 561 | #### 销毁goroutine 562 | 563 | 一个goroutine的销毁操作并不能确保 happen before 程序中的任何事件,比如下面例子 564 | 565 | ``` 566 | var a string 567 | 568 | func hello() { 569 | go func() { a = "hello" }() 570 | print(a) 571 | } 572 | 573 | ``` 574 | 575 | 如上代码 goroutine内对变量a的赋值并没有加任何同步措施,所以并能不保证hello函数所在的goroutine对变量a的赋值可见。如果要确保一个goroutine对变量的修改对其他goroutine可见,必须使用一定的同步机制,比如锁、通道来建立对同一个变量读写的偏序关系。 576 | 577 | #### 有缓冲 channel 578 | 579 | 在有缓冲的通道时候向通道写入一个数据总是 happen before 这个数据被从通道中读取完成,如下例子: 580 | 581 | ``` 582 | package main 583 | 584 | import ( 585 | "fmt" 586 | ) 587 | 588 | var c = make(chan int, 10) 589 | var a string 590 | 591 | func f() { 592 | a = "hello, world" //1 593 | c <- 0 //2 594 | } 595 | 596 | func main() { 597 | go f() //3 598 | <-c //4 599 | fmt.Print(a) //5 600 | } 601 | 602 | ``` 603 | 604 | 如上代码运行后可以确保输出"hello, world",这里对变量a的写操作(1) happen before 向通道写入数据的操作(2),而向通道写入数据的操作(2)happen before 从通道读取数据完成的操作(4),而步骤(4)happen before 步骤(5)的打印输出。 605 | 606 | 另外关闭通道的操作 happen before 从通道接受0值(关闭通道后会向通道发送一个0值),修改上面代码(2)如下: 607 | 608 | ``` 609 | package main 610 | 611 | import ( 612 | "fmt" 613 | ) 614 | 615 | var c = make(chan int, 10) 616 | var a string 617 | 618 | func f() { 619 | a = "hello, world" //1 620 | close(c) //2 621 | } 622 | 623 | func main() { 624 | go f() //3 625 | <-c //4 626 | fmt.Print(a) //5 627 | } 628 | ``` 629 | 在有缓冲通道中通过向通道写入一个数据总是 happen before 这个数据被从通道中读取完成,这个happen before规则使多个goroutine中对共享变量的并发访问变成了可预见的串行化操作。 630 | 631 | #### 无缓冲 channel 632 | 633 | 对应无缓冲的通道来说从通道接受(获取叫做读取)元素 happen before 向通道发送(写入)数据完成,看下下面代码: 634 | 635 | ``` 636 | package main 637 | 638 | import ( 639 | "fmt" 640 | ) 641 | 642 | var c = make(chan int) 643 | var a string 644 | 645 | func f() { 646 | a = "hello, world" //1 647 | <-c //2 648 | } 649 | 650 | func main() { 651 | go f() //3 652 | c <- 0 //4 653 | fmt.Print(a) //5 654 | } 655 | 656 | ``` 657 | 658 | 如上代码运行也可保证输出"hello, world",注意改程序相比上一个片段,通道改为了无缓冲,并向通道发送数据与读取数据的步骤(2)(4)调换了位置。 659 | 660 | 在这里写入变量a的操作(1)happen before 从通道读取数据完毕的操作(2),而从通道读取数据的操作 happen before 向通道写入数据完毕的操作(4),而步骤(4) happen before 打印输出步骤(5)。 661 | 662 | 注:在无缓冲通道中从通道读取数据的操作 happen before 向通道写入数据完毕的操作,这个happen before规则使多个goroutine中对共享变量的并发访问变成了可预见的串行化操作。 663 | 664 | 如上代码如果换成有缓冲的通道,比如c = make(chan int, 1)则就不能保证一定会输出"hello, world"。 665 | 666 | #### channel 规则抽象 667 | 668 | 从容量为C的通道接受第K个元素 happen before 向通道第k+C次写入完成,比如从容量为1的通道接受第3个元素 happen before 向通道第3+1次写入完成。 669 | 670 | 这个规则对有缓冲通道和无缓冲通道的情况都适用,有缓冲的通道可以实现信号量计数的功能,比如通道的容量可以认为是最大信号量的个数,通道内当前元素个数可以认为是剩余的信号量个数,向通道写入(发送)一个元素可以认为是获取一个信号量,从通道读取(接受)一个元素可以认为是释放一个信号量,所以有缓冲的通道可以作为限制并发数的一个通用手段: 671 | 672 | ``` 673 | package main 674 | 675 | import ( 676 | "fmt" 677 | "time" 678 | ) 679 | 680 | var limit = make(chan int, 3) 681 | 682 | func sayHello(index int){ 683 | fmt.Println(index ) 684 | } 685 | 686 | var work []func(int) 687 | func main() { 688 | 689 | work := append(work,sayHello,sayHello,sayHello,sayHello,sayHello,sayHello) 690 | 691 | for i, w := range work { 692 | go func(w func(int),index int) { 693 | limit <- 1 694 | w(index) 695 | <-limit 696 | }(w,i) 697 | } 698 | 699 | time.Sleep(time.Second * 10) 700 | } 701 | 702 | ``` 703 | 704 | 如上代码main goroutine里面为work列表里面的每个方法的执行开启了一个单独的goroutine,这里有6个方法,正常情况下这7个goroutine可以并发运行,但是本程序使用缓存大小为3的通道来做并发控制,导致同时只有3个goroutine可以并发运行。 705 | 706 | ### 锁(locks) 707 | 708 | sync包实现了两个锁类型,分别为 sync.Mutex(互斥锁)和 sync.RWMutex(读写锁)。 709 | 710 | 对应任何sync.Mutex or sync.RWMutex类型的遍历I来说调用n次 l.Unlock() 操作 happen before 调用m次l.Lock()操作返回,其中n Yes, if you call RawSyscall you may block other goroutines from running. The system monitor may start them up after a while, but I think there are cases where it won't. I would say that Go programs should always call Syscall. RawSyscall exists to make it slightly more efficient to call system calls that never block, such as getpid. But it's really an internal mechanism. 136 | 137 | RawSyscall 只是为了在执行那些一定不会阻塞的系统调用时,能节省两次对 runtime 的函数调用消耗。 138 | 139 | ### vdso 140 | 141 | vdso 可以认为是一种特殊的调用,在使用时,没有本文开头的用户态到内核态的切换,引用一段参考资料: 142 | 143 | > 用来执行特定的系统调用,减少系统调用的开销。某些系统调用并不会向内核提交参数,而仅仅只是从内核里请求读取某个数据,例如gettimeofday(),内核在处理这部分系统调用时可以把系统当前时间写在一个固定的位置(由内核在每个时间中断里去完成这个更新动作),mmap映射到用户空间。这样会更快速,避免了传统系统调用模式INT 0x80/SYSCALL造成的内核空间和用户空间的上下文切换。 144 | 145 | ```go 146 | // func gettimeofday(tv *Timeval) (err uintptr) 147 | TEXT ·gettimeofday(SB),NOSPLIT,$0-16 148 | MOVQ tv+0(FP), DI 149 | MOVQ $0, SI 150 | MOVQ runtime·__vdso_gettimeofday_sym(SB), AX 151 | CALL AX 152 | 153 | CMPQ AX, $0xfffffffffffff001 154 | JLS ok7 155 | NEGQ AX 156 | MOVQ AX, err+8(FP) 157 | RET 158 | ok7: 159 | MOVQ $0, err+8(FP) 160 | RET 161 | ``` 162 | 163 | ## 系统调用管理 164 | 165 | 先是系统调用的定义文件: 166 | 167 | ```shell 168 | /syscall/syscall_linux.go 169 | ``` 170 | 171 | 可以把系统调用分为三类: 172 | 173 | 1. 阻塞系统调用 174 | 2. 非阻塞系统调用 175 | 3. wrapped 系统调用 176 | 177 | 以 Madvise 为例,阻塞系统调用会定义成下面这样的形式: 178 | 179 | ```go 180 | //sys Madvise(b []byte, advice int) (err error) 181 | ``` 182 | 183 | EpollCreate 为例,非阻塞系统调用: 184 | 185 | ```go 186 | //sysnb EpollCreate(size int) (fd int, err error) 187 | ``` 188 | 189 | 然后,根据这些注释,mksyscall.pl 脚本会生成对应的平台的具体实现。mksyscall.pl 是一段 perl 脚本,感兴趣的同学可以自行查看,这里就不再赘述了。 190 | 191 | 看看阻塞和非阻塞的系统调用的生成结果: 192 | 193 | ```go 194 | func Madvise(b []byte, advice int) (err error) { 195 | var _p0 unsafe.Pointer 196 | if len(b) > 0 { 197 | _p0 = unsafe.Pointer(&b[0]) 198 | } else { 199 | _p0 = unsafe.Pointer(&_zero) 200 | } 201 | _, _, e1 := Syscall(SYS_MADVISE, uintptr(_p0), uintptr(len(b)), uintptr(advice)) 202 | if e1 != 0 { 203 | err = errnoErr(e1) 204 | } 205 | return 206 | } 207 | 208 | func EpollCreate(size int) (fd int, err error) { 209 | r0, _, e1 := RawSyscall(SYS_EPOLL_CREATE, uintptr(size), 0, 0) 210 | fd = int(r0) 211 | if e1 != 0 { 212 | err = errnoErr(e1) 213 | } 214 | return 215 | } 216 | ``` 217 | 218 | 显然,标记为 sys 的系统调用使用的是 Syscall 或者 Syscall6,标记为 sysnb 的系统调用使用的是 RawSyscall 或 RawSyscall6。 219 | 220 | wrapped 的系统调用是怎么一回事呢? 221 | 222 | ```go 223 | func Rename(oldpath string, newpath string) (err error) { 224 | return Renameat(_AT_FDCWD, oldpath, _AT_FDCWD, newpath) 225 | } 226 | ``` 227 | 228 | 可能是觉得系统调用的名字不太好,或者参数太多,我们就简单包装一下。没啥特别的。 229 | 230 | ## runtime 中的 SYSCALL 231 | 232 | 除了上面提到的阻塞非阻塞和 wrapped syscall,runtime 中还定义了一些 low-level 的 syscall,这些是不暴露给用户的。 233 | 234 | 提供给用户的 syscall 库,在使用时,会使 goroutine 和 p 分别进入 Gsyscall 和 Psyscall 状态。但 runtime 自己封装的这些 syscall 无论是否阻塞,都不会调用 entersyscall 和 exitsyscall。 虽说是 “low-level” 的 syscall, 235 | 不过和暴露给用户的 syscall 本质是一样的。这些代码在 `runtime/sys_linux_amd64.s` 中,举个具体的例子: 236 | 237 | ```go 238 | TEXT runtime·write(SB),NOSPLIT,$0-28 239 | MOVQ fd+0(FP), DI 240 | MOVQ p+8(FP), SI 241 | MOVL n+16(FP), DX 242 | MOVL $SYS_write, AX 243 | SYSCALL 244 | CMPQ AX, $0xfffffffffffff001 245 | JLS 2(PC) 246 | MOVL $-1, AX 247 | MOVL AX, ret+24(FP) 248 | RET 249 | 250 | TEXT runtime·read(SB),NOSPLIT,$0-28 251 | MOVL fd+0(FP), DI 252 | MOVQ p+8(FP), SI 253 | MOVL n+16(FP), DX 254 | MOVL $SYS_read, AX 255 | SYSCALL 256 | CMPQ AX, $0xfffffffffffff001 257 | JLS 2(PC) 258 | MOVL $-1, AX 259 | MOVL AX, ret+24(FP) 260 | RET 261 | ``` 262 | 263 | 下面是所有 runtime 另外定义的 syscall 列表: 264 | 265 | ```go 266 | #define SYS_read 0 267 | #define SYS_write 1 268 | #define SYS_open 2 269 | #define SYS_close 3 270 | #define SYS_mmap 9 271 | #define SYS_munmap 11 272 | #define SYS_brk 12 273 | #define SYS_rt_sigaction 13 274 | #define SYS_rt_sigprocmask 14 275 | #define SYS_rt_sigreturn 15 276 | #define SYS_access 21 277 | #define SYS_sched_yield 24 278 | #define SYS_mincore 27 279 | #define SYS_madvise 28 280 | #define SYS_setittimer 38 281 | #define SYS_getpid 39 282 | #define SYS_socket 41 283 | #define SYS_connect 42 284 | #define SYS_clone 56 285 | #define SYS_exit 60 286 | #define SYS_kill 62 287 | #define SYS_fcntl 72 288 | #define SYS_getrlimit 97 289 | #define SYS_sigaltstack 131 290 | #define SYS_arch_prctl 158 291 | #define SYS_gettid 186 292 | #define SYS_tkill 200 293 | #define SYS_futex 202 294 | #define SYS_sched_getaffinity 204 295 | #define SYS_epoll_create 213 296 | #define SYS_exit_group 231 297 | #define SYS_epoll_wait 232 298 | #define SYS_epoll_ctl 233 299 | #define SYS_pselect6 270 300 | #define SYS_epoll_create1 291 301 | ``` 302 | 303 | 这些 syscall 理论上都是不会在执行期间被调度器剥离掉 p 的,所以执行成功之后 goroutine 会继续执行,而不像用户的 goroutine 一样,若被剥离 p 会进入等待队列。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | 主要对 Golangv1.13 版本的 runtime 进行源码分析,本项目参考了一些书籍和其他源码分析的项目: 4 | 5 | - [golang-notes](https://github.com/cch123/golang-notes) 6 | - [浅谈 Go 语言实现原理](https://draveness.me/golang/) 7 | - 深入解析 Go 内核实现 8 | - Go 1.5 源码剖析 9 | - Go 学习笔记 10 | 11 | 本项目目录: 12 | 13 | - [Go 内存管理](https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86.md) 14 | - [Go 垃圾回收](https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.md) 15 | - [Go 协程调度——基本原理与初始化](https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20%E5%8D%8F%E7%A8%8B%E8%B0%83%E5%BA%A6%E2%80%94%E2%80%94%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86%E4%B8%8E%E5%88%9D%E5%A7%8B%E5%8C%96.md) 16 | - [Go 协程调度——PMG 调度细节分析](https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20%E5%8D%8F%E7%A8%8B%E8%B0%83%E5%BA%A6%E2%80%94%E2%80%94PMG%20%E8%B0%83%E5%BA%A6%E7%BB%86%E8%8A%82%E5%88%86%E6%9E%90.md) 17 | - [Go 网络调用 netpoll](https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20%E7%BD%91%E7%BB%9C%E8%B0%83%E7%94%A8%20netpoll.md) 18 | - [Go 系统调用](https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8.md) 19 | - [Go 内存一致性模型](https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20%E5%86%85%E5%AD%98%E4%B8%80%E8%87%B4%E6%80%A7%E6%A8%A1%E5%9E%8B.md) 20 | - [Go Semaphore](https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20Semaphore.md) 21 | - [Go Sync——Mutex](https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20Sync%E2%80%94%E2%80%94Mutex.md) 22 | - [Go Sync](https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20Sync.md) 23 | - [Go interface](https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20interface.md) 24 | - [Go interface 反射](https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20interface%20%E5%8F%8D%E5%B0%84.md) 25 | - [Go Channel](https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20Channel.md) 26 | - [Go Select](https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20Select.md) 27 | - [Go Slice](https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20Slice.md) 28 | - [Go Map](https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20Map.md) 29 | - [Go Defer](https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20Defer.md) 30 | - [Go panic 和 recover](https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20panic%20%E5%92%8C%20recover.md) -------------------------------------------------------------------------------- /img/PMG1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/PMG1.png -------------------------------------------------------------------------------- /img/PMG2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/PMG2.png -------------------------------------------------------------------------------- /img/PMG3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/PMG3.png -------------------------------------------------------------------------------- /img/_type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/_type.png -------------------------------------------------------------------------------- /img/arenas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/arenas.png -------------------------------------------------------------------------------- /img/arenas2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/arenas2.jpg -------------------------------------------------------------------------------- /img/arenas2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/arenas2.png -------------------------------------------------------------------------------- /img/arenas3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/arenas3.jpg -------------------------------------------------------------------------------- /img/arenas4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/arenas4.jpg -------------------------------------------------------------------------------- /img/bucket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/bucket.png -------------------------------------------------------------------------------- /img/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/cat.png -------------------------------------------------------------------------------- /img/cat1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/cat1.png -------------------------------------------------------------------------------- /img/cat3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/cat3.png -------------------------------------------------------------------------------- /img/copy1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/copy1.jpg -------------------------------------------------------------------------------- /img/copy2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/copy2.jpg -------------------------------------------------------------------------------- /img/cycle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/cycle.jpg -------------------------------------------------------------------------------- /img/duck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/duck.png -------------------------------------------------------------------------------- /img/duck2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/duck2.png -------------------------------------------------------------------------------- /img/duck3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/duck3.png -------------------------------------------------------------------------------- /img/duck4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/duck4.png -------------------------------------------------------------------------------- /img/duck5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/duck5.png -------------------------------------------------------------------------------- /img/duck6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/duck6.png -------------------------------------------------------------------------------- /img/falseshare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/falseshare.png -------------------------------------------------------------------------------- /img/gc1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/gc1.png -------------------------------------------------------------------------------- /img/gen1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/gen1.jpg -------------------------------------------------------------------------------- /img/gen2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/gen2.jpg -------------------------------------------------------------------------------- /img/hashgrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/hashgrow.png -------------------------------------------------------------------------------- /img/hashgrow1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/hashgrow1.png -------------------------------------------------------------------------------- /img/hmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/hmap.png -------------------------------------------------------------------------------- /img/interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/interface.png -------------------------------------------------------------------------------- /img/interface1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/interface1.png -------------------------------------------------------------------------------- /img/interface3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/interface3.png -------------------------------------------------------------------------------- /img/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/map.png -------------------------------------------------------------------------------- /img/marksweep.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/marksweep.jpg -------------------------------------------------------------------------------- /img/marksweep1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/marksweep1.jpg -------------------------------------------------------------------------------- /img/mem.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/mem.jpg -------------------------------------------------------------------------------- /img/mem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/mem.png -------------------------------------------------------------------------------- /img/mesi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/mesi.png -------------------------------------------------------------------------------- /img/mesi2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/mesi2.png -------------------------------------------------------------------------------- /img/mesi3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/mesi3.jpg -------------------------------------------------------------------------------- /img/mesi4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/mesi4.jpg -------------------------------------------------------------------------------- /img/methodset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/methodset.png -------------------------------------------------------------------------------- /img/mheap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/mheap.jpg -------------------------------------------------------------------------------- /img/mspan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/mspan.jpg -------------------------------------------------------------------------------- /img/mutex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/mutex.png -------------------------------------------------------------------------------- /img/preem.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/preem.jpg -------------------------------------------------------------------------------- /img/span1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/span1.jpg -------------------------------------------------------------------------------- /img/stackmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/stackmap.png -------------------------------------------------------------------------------- /img/stackmap1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/stackmap1.png -------------------------------------------------------------------------------- /img/tiny1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/tiny1.png -------------------------------------------------------------------------------- /img/tiny2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/tiny2.png -------------------------------------------------------------------------------- /img/volatile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/volatile.png -------------------------------------------------------------------------------- /img/volatile1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/volatile1.png -------------------------------------------------------------------------------- /img/volatile3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/volatile3.png -------------------------------------------------------------------------------- /img/volatile4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/volatile4.png -------------------------------------------------------------------------------- /img/volatile5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/volatile5.png -------------------------------------------------------------------------------- /img/waiting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/waiting.png -------------------------------------------------------------------------------- /img/writeb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/writeb.jpg -------------------------------------------------------------------------------- /img/writeb2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoYang90/Golang-Internal-Notes/85a21050d9da15c48ea0da73777328d21954655d/img/writeb2.jpg --------------------------------------------------------------------------------