【Go】源码
基础类型
slice
Map
Channel
chansend源码
- 是否nil channel,是的话直接阻塞
- 是否有被阻塞的接收者,有的话直接交付数据,返回
- 没有的话,看缓冲区是否满了,没满放入缓冲,返回
- 满了,阻塞,等待接收者唤醒
- 被唤醒,做些清操作
chanrecv源码
Goroutine泄露与内存逃逸
如果channel使用不当,goroutine被阻塞后没有唤醒会导致goroutine泄露:
- 只发送不接收,发送者阻塞,会导致发送者goroutine泄露
- 只接收不发送,接受者阻塞,会导致接收者goroutine泄露
- 读写nil都会导致goroutine泄露
- 唯一例外的是业务层面上的goroutine长时间运行
- 如果用channel发送指针,那么必然发生内存逃逸
原生库
context
sync
sync.Map
read
只读数据readOnly
dirty
读写数据,操作dirty
需要用mu
进行加锁来保证并发安全misses
用于统计有多少次读取read
没有命中amended
用于标记read
和dirty
的数据是否一致
Load
- 先从
read
读 - 没有命中,到
dirty
读取数据,同时调用missLocked()
增加misses
- 当
misses
大于len(dirty)
时(read
和dirty
的数据相差太大),会将dirty
的数据赋值给read
,dirty
会被置空
1 |
|
1 |
|
Store
- 直接到
read
修改数据,修改成功则直接返回 - 如果
key
不存在,到dirty
找数据,dirty
存在key
则修改, - 不存在则新增,同时还要将
read
中的amended
标记为 true(read
和dirty
的数据已经不一致)
1 |
|
Range
Range
会保证read
和dirty
是数据同步的- 回调函数返回 false 会导致迭代中断
1 |
|
Delete
延迟删除
的机制,- 首先到 查找
read
是否存在key
,如果存在则执行entry.delete
进行软删除,通过 CAS 将指针entry.p (存放数据的指针)
置为 nil,减少锁开销提高并发性能。 read
找不到key
且amended
为 true 才会通过delete
进行硬删除,这个阶段是会加锁的。
1 |
|
sync.Mutex && sync.RWMutex
- state用来控制锁状态的核心。
- sema处理沉睡/唤醒的信号量
- runtime_SemacquireMutex:sema加1并且挂起goroutine
- runtime_Semrelease:sema减1并唤起sema上等待的一个goroutine
- 总体流程
- 先进性一个人CAS操作。如果锁空闲,并且没有其他协程竞争,便直接成功。
- 否则自旋几次,如果成功,不用加入队列。
- 否则加入队列
- 从队列中被唤醒
- 正常模式:和新来的协程一起竞争锁,但是大概率失败(等待队列中的协程等待时间超过1ms,锁会变成饥饿模式)
- 饥饿模式:肯定拿到锁
- 可以基于RWMutex实现double-check
- 加读锁先检查一遍
- 释放读锁
- 加写锁
- 在检查一遍
sync.Once
- 利原子操作实现读写锁的double-check
sync.Pool
PMG调度模型实现的Pool。
- 每个P带有一个poolLocal对象
- 每个poolLocal又一个private和shared
- shared指向的是一个poolChain,poolChain的数据会被别的P偷走
- poolChain是一个链表+ring buffer的双重结构
- 从总体上看,是一个双链表
- 从单个节点来说,它指向的是一个ring buffer(后面节点的ring buffer长度是前面节点ring buffer长度的二倍)
sync.Pool.Get
- private是否可用,可用直接返回
- 不可用从自己的poolChain里尝试获取
- 从
head
开始找(当前头指向的是最近创建的ringbuffer) - 在ringbuffer队列中从队头往队尾找
- 从
- 找不到尝试从别的P里面偷(偷的过程是全局并发的)
- 偷是从
tail
队尾开始找
- 偷是从
- 偷不到,会从victim中找
- victim找不到,会创建新的
1 |
|
sync.Pool.Put
- private没被占用,直接放private
- 否则放入shared(poolChain)
- poolChain的HEAD没有创建,就创建一个HEAD,然后穿件一个容量为8的ring buffer,把数据丢过去
- poolChain的HEAD指向的ring buffer没满,数据丢到ring buffer
- poolChain的HEAD指向的ring buffer满了,创建新节点(2倍容量的ring buffer),把数据丢过去
1 |
|
sync.Pool存粹依赖GC 进行淘汰。核心在于locals和victim
- locals会被挪过去变成victim
- victim会被GC时直接回收掉,如果victim里的对象被再次使用,则会被丢到locals
sync.WaitGroup
- Add:state1的高32位自增1
- Done:state1的高32位减1,就是Add(-1)
- Wait:state1低32位自增1,同时利用state2和runtime_Semacquire将当前goroutine挂起
atomic
atomic.Value
原子读写只提供了 int32
,int64
,uint32
,uint64
,uintptr
和 unsafe.Pointer
数据类型。
atomic.Value
的零值为 nil,且使用后不允许被拷贝。写入值后 ifaceWords
中 typ
保存数据类型,data
保存值。
1 |
|
Store
- 不能保存 nil
Store
固定类型,后续操作必须使用相同的数据类型,否则会 panic- 首次
Store
会调用runtime_procPin()
禁止当前 P 被抢占,然后 CAS 抢占乐观锁 ,将typ
修改为中间值unsafe.Pointer(^uintptr(0))
if uintptr(typ) == ^uintptr(0)
==true
则表示还在抢占锁中,抢到锁就会修改typ
和data
1 |
|
Load
- ifaceWords未写入,返回nil
1 |
|
reflect
reflect.ValueOf: 用于操作值,部分值可以被反射修改
reflect.TypeOf: 用于操作类信息,只能读取(可以通过reflect.Value得到)
reflect.Kind: 用于判断类型
1 |
|
reflect.FuncInfo
1 |
|
unsafe
1 |
|
os/signal
每个平台的信号定义或许有些不同。下面列出了POSIX中定义的信号。 Linux 使用34-64信号用作实时系统中。
在POSIX.1-1990标准中定义的信号列表
1 |
|
kill pid 与 kill -9 pid的区别
kill pid的作用是向进程号为pid的进程发送SIGTERM(这是kill默认发送的信号),该信号是一个结束进程的信号且可以被应用程序捕获。若应用程序没有捕获并响应该信号的逻辑代码,则该信号的默认动作是kill掉进程。这是终止指定进程的推荐做法。
kill -9 pid
则是向进程号为pid的进程发送 SIGKILL(该信号的编号为9),从本文上面的说明可知,SIGKILL既不能被应用程序捕获,也不能被阻塞或忽略,其动作是立即结束指定进程。通俗地说,应用程序根本无法“感知”SIGKILL信号,它在完全无准备的情况下,就被收到SIGKILL信号的操作系统给干掉了,显然,在这种“暴力”情况下,应用程序完全没有释放当前占用资源的机会。事实上,SIGKILL信号是直接发给init进程的,它收到该信号后,负责终止pid指定的进程。在某些情况下(如进程已经hang死,无法响应正常信号),就可以使用 kill -9
来结束进程。
应用程序如何优雅退出?
Linux Server端的应用程序经常会长时间运行,在运行过程中,可能申请了很多系统资源,也可能保存了很多状态,在这些场景下,我们希望进程在退出前,可以释放资源或将当前状态dump到磁盘上或打印一些重要的日志,也就是希望进程优雅退出(exit gracefully)。
Go中的Signal发送和处理
- golang中对信号的处理主要使用os/signal包中的两个方法:
- notify方法用来监听收到的信号
- stop方法用来取消监听
监听全部信号
1 |
|
监听指定信号
1 |
|
优雅退出go守护进程
1 |
|