type slice struct {
array unsafe.Pointer
len int
cap int
}
make创建,会从底层分配一个数组,数组长度就是指定的容量
可以使用语句slice := array[5:7]
来创建slice, cap就是剩下没有占用的数组长度
扩容的底层其实是重新分配空间, 再将原slice的数据拷贝到新的slice中, 然后返回新的slice
首先会进行预估扩容
从一个长度容量为2的int切片中, 使用append一次性插入4个int
那么这里预估扩容会先预估成容量为6, 进行判断, 如果大于两倍的旧容量, 则新切片的容量为6, 如果小于两倍的旧容量, 那么新容量就是两倍的旧容量, 也就是4
func main() {
s := []int{1,2}
s = append(s,3,4,5,6)
fmt.Printf("len=%d, cap=%d",len(s),cap(s))
}
但是还有另一种情况
刚刚我们说的是int类型的切片, 它默认是我们的机器字长, 也就是8个字节, 但是切片类型是byte这样的, 小于八个字节, 会进行下一步
如果我从长度容量为200的byte切片中, 插入一个byte
按照预估扩容, 预估容量值为201, 小于200*2, 所以新的容量应该是400
但是并不是这样, 新的容量会是412
这里对于类型小于机器字长的类型, 会采用一种向上对齐的方式来分配容量
在go语言里, 内存都是按一些8的整数倍切成了不同大小的内存块
按照刚刚我们应该去申请一个400容量大小的byte切片, 数据部分要由一个400字节的连续内存, 但是因为没有400字节的内存块, 但是有412字节的内存块, go就会索性让你多扩充一点, 把整个412字节的内存都给你, 你的容量就是412了, 也就是一个向上取整的过程
type stringStruct struct {
str unsafe.Pointer
len int
}
在有中文的情况下使用for i 和 for range的方式, 遍历效果不一样, 是因为string使用了utf-8的变长编码, 一个字符占多少字节是有该字符的头几位决定
for i的形式, 会按照字节流读取, for range 会按照字符流读取
string在go语言里被分配到了只读内存, 不能进行修改
如需要修改的话,先转化为[]byte, 转化过程, 其实新开辟了一个等长的[]byte空间进行了拷贝, []byte可以修改, 改完之后再转回string即可
但是在[]byte转string的时候, 却不一定会发生拷贝, 在一些临时使用string类型的时候, 将[]byte转化为string, 会直接讲string的str指针指向该byte
func main() {
m := map[string]int{"abc":1}
b := []byte{'a','b','c'}
value,has := m[string(b)]
}
func main() {
b := []byte{'b'}
abc := "a" + string(b) + "c"
}
func main() {
b := []byte{'acb'}
a := string(b) == "abc"
}
因为64位的计算机, 在读取内存的时候, 是一次性读取八个字节出来, 所以传入的地址都是8的倍数, 如果想从地址1开始读8个字节, 就只能先从地址0开始, 读取八个字节, 再从地址8开始, 读取八个字节. 拼成从地址1读取八个字节
golang编译器为了减少这样的情况, 会把各种类型的数据安排到合适的地址, 占用合适的长度
同一种结构体, 内部成员一样, 但是顺序不同, 结构体占用的大小不一样, 正是因为内存对齐导致的
类型占用的初始地址, 必须是对齐边界的整数倍, 占用大小也需是对齐边界的整数倍
结构体内部成员的对齐边界取决于成员类型, 结构体的对齐边界取决于内部成员的最大对齐边界
因为空结构体不占据内存空间,因此被广泛作为各种场景下的占位符使用。
map 作为集合(Set)使用时,可以将值类型定义为空结构体,仅作为占位符使用即可。
不发送数据的信道(channel)
使用 channel 不需要发送任何的数据,只用来通知子协程(goroutine)执行任务,或只用来控制协程并发度。
包含了键值对的数目, 桶的数目
type hmap struct {
count int // count 字段表征了 map 目前的元素数目
flags uint8 //flag 字段标志 map 的状态, 如 map 当前正在被遍历或正在被写入
B uint8 // 哈希桶数目2^B
noverflow uint16 // noverflow 是溢出桶的数目
hash0 uint32 // hash0是随机哈希种子
buckets unsafe.Pointer // 是指向当前哈希桶的指针
oldbuckets unsafe.Pointer // 是当桶扩容时指向旧桶的指针
nevacuate uintptr // 是渐进式扩容时,桶进行调整时指示的搬迁进度
extra *mapextra // optional fields
}
hash的方式目前主要就两种
buckets对应的数据结构
type bmap struct {
topbits [8]uint8
keys [8]keytype
elems [8]elemtype
overflow uintptr
}
每个bucket可以存储8个键值对。
type mapextra struct {
overflow *[]*bmap // 目前桶中所有溢出桶的地址
oldoverflow *[]*bmap // 旧桶中所有溢出桶的地址
nextOverflow *bmap // 下一个空闲溢出桶地址
}
这里的下一个空闲溢出桶是这样的, 当B>4时, go会预先分配2^(B-4)个溢出桶作为备用
冲突太多,导致查找速率变慢,就需要对哈希表扩容,分配更多的桶,将旧桶里的数据分散到新桶中间,并且使用渐进式扩容,不是一次性将所有的旧桶数据转移到新桶,而是在每次操作map时,一次次搬运到新桶中。
负载因子=key的数目/桶的数量
当负载因子大于6.5时,再进行插入操作,会触发增量扩容
如图,此时已经有七个key,负载因子大于6.5,再次执行插入,触发扩容,新数据直接进新桶
hmap数据结构中oldbuckets成员指身原bucket,而buckets指向了新申请的bucket。新的键值对被插入新的bucket中。
后续对map的访问操作会触发迁移,将oldbuckets中的键值对逐步的搬迁过来。当oldbuckets中的键值对全部搬迁完毕后,删除oldbuckets。
溢出桶太多,也会触发扩容,将松散的key-value重新排列一次,减少溢出桶的数目,执行过很多del操作,会出现这样的情况
如果B<=15, 溢出桶数量 >= 2^B,就会触发等量扩容
如果B>15, 溢出桶只要 >=2^15,就会触发等量扩容
把旧桶的数据从搬运到新桶中,会重新排列,会排列的更加紧凑:
不增加buckets的数量
不是线程安全的, 但是go语言提供了一个sync.Map来用于多个goroutine来使用的Map
type Map struct {
mu Mutex
read atomic.Value // readOnly
dirty map[interface{}]*entry //包含新写入的 key,并且包含 read 中所有未被删除的 key
misses int //统计有多少次读取 read 没有被命中。每次 read 读取失败后,misses 的计数加 1
}
// Map.read 属性实际存储的是 readOnly。
type readOnly struct {
m map[interface{}]*entry
amended bool //用于标记 read 和 dirty 的数据是否一致
}
type entry struct {
p unsafe.Pointer // *interface{}
}
read 和 dirty 各自维护一套 key,key 指向的都是同一个 value。
也就是说,只要修改了这个 entry,对 read 和 dirty 都是可见的
现在向map操作Store(“hello”,“not world”),read中有对应的键,不会上锁访问dirty,直接替换
如果访问read中不存在的key,misses就加1,当misses大于某一个数时,就将dirty表升级为read表
dirty表置为nil
如果再次新增其他的键值对时,dirty表会进行重建,会复制read表中的数据
如果在dirty重建的时候,删除了某个键值对,会让entry指向nil,之后dirty重建,dirty中的key5也会复制,entry都指向nil, 但是其实这里的key5是不需要存在的
为了避免在重建dirty的时候复制了已经删除的key,会在dirty为nil的时候,删除某一个key时,进行entry指向expunged的操作
这样在重建dirty的时候回去判断该key下面是否真的有value
再次进行key5存储的时候,就需要看read表中有没有,如果有,是否标志为expunged,如果是,那么要去dirty表上锁,插入key5和与之对应的entry,同时更新read表中的key5的entry
首先函数被编译成一条条机器指令,放入虚拟内存地址中的代码段
函数调用另一个函数,会生成一条call指令
call指令会做两件事情, 一个是入栈返回地址, 一个是跳转到别的函数的代码段的入口
一个函数的开头会做三件事情, 挪动栈指针到预分配的栈帧顶部
将bp寄存器的调用者栈基存入栈中
更改栈基位置
函数结束的时候会执行这几步
恢复调用者栈基地址
释放自己的栈帧空间
然后就是执行ret指令
ret指令干两件事情
弹出返回地址
跳转到返回地址
函数的会先将返回值空间入栈, 赋值0值
再将参数从右往左入栈
这里就一个参数,所以入栈a, a为0
注册完函数的这些参数和返回值后
就可以初始化incr函数的局部变量b
执行a++, a没有在函数incr的函数栈帧中
执行b=a
在return的时候, 会先对返回值空间赋值, 再执行defer函数, 所以先赋值
然后执行defer中的a++, b++
incr结束返回值就是1
值得注意的是, 命名返回值不是局部变量, 是函数的返回值空间有了名字, 可以被return之外的语句修改
main函数调用incr, 入栈返回值空间和参数
但是没有局部变量, 这里的b指向的是返回值空间
incr中返回的是a,但是也只是将a的值赋值给返回值空间
此时a++不影响返回值空间
但是b++影响, 因为b指向的就是返回值空间
函数对应的是一个Function Value的头等对象, 其本质是一个指针f, 指向funcval结构体
而funcval结构体中有个fn指针, 这个指针是指向了函数指令的入口地址
函数赋值给变量之后, 会让f1和f2指向同一个funcval结构体, 但是在闭包的情况, 不会指向一个funcval
在第一次调用create时, 返回时生成一个funcval结构体,地址为addr2, 由于该匿名函数用了外层函数的参数, 所以生成捕获列表, 捕获了c变量
在第一次调用create时, 返回时也生成一个funcval结构体, 地址为addr2, 也生成捕获列表, 捕获了c变量
获取捕获列表的方式, 是通过寄存器保存funcval地址, 然后通过该寄存器的地址通过偏移找到捕获变量
这里的捕获变量没有遭到修改, 所以直接值拷贝到捕获列表中
但是要是捕获变量会修改, 就只会拷贝它的地址到捕获列表里
捕获变量真正的存储的地方逃逸到堆上
也会将返回值空间的数据拷贝到堆上来, 闭包使用该地址
但是在外部函数真正做返回的时候, 会把堆上的数据拷贝会栈上的返回值空间
type hchan struct {
qcount uint // 当前队列中剩余元素个数
dataqsiz uint // 环形队列长度,即可以存放的元素个数
buf unsafe.Pointer // 环形队列指针
elemsize uint16 // 每个元素的大小
closed uint32 // 标识关闭状态
elemtype *_type // 元素类型
sendx uint // 队列下标,指示元素写入时存放到队列中的位置
recvx uint // 队列下标,指示元素从队列的该位置读出
recvq waitq // 等待读消息的goroutine队列
sendq waitq // 等待写消息的goroutine队列
lock mutex // 互斥锁,chan不允许并发读写
}
chan内部实现了一个环形队列作为其缓冲区,队列的长度是创建chan时指定的。
下图展示了一个可缓存6个元素的channel示意图:
从channel读数据,如果channel缓冲区为空或者没有缓冲区,当前goroutine会被阻塞。
向channel写数据,如果channel缓冲区已满或者没有缓冲区,当前goroutine会被阻塞。
被阻塞的goroutine将会挂在channel的等待队列中:
关闭channel时会把recvq中的G全部唤醒,本该写入G的数据位置为nil。把sendq中的G全部唤醒,但这些G会panic
通过range可以持续从channel中读出数据,好像在遍历一个数组一样,当channel中没有数据时会阻塞当前goroutine,与读channel时阻塞处理机制一样。
func chanRange(chanName chan int) {
for e := range chanName {
fmt.Printf("Get element from chan: %d\n", e)
}
}
注意:如果向此channel写数据的goroutine退出时,系统检测到这种情况后会panic,否则range将会永久阻塞。
每种类型都对应着一些类型元数据, 内置类型的类型元数据里面有一些整体属性, 不同的类型有不同的其他描述信息, 而自定义类型还会有关于包路径, 方法数目, 方法等一些信息
别名生成的都是执行同一个类型元数据, 而自定义类型是指向不同的类型元数据
对应eface
对应iface结构体, 包含itab和数据, itab包含了该接口的类型元数据, 和该接口的动态类型元数据
分为四种情况,:
空接口断言成具体类型
非空接口断言成具体类型
空接口断言成非空接口
非空接口断言成非空接口
加锁时,如果当前Locked位为1,说明该锁当前由其他协程持有,尝试加锁的协程并不是马上转入阻塞,而是会持续的探测Locked位是否变为0,这个过程即为自旋过程。
自旋时间很短,但如果在自旋过程中发现锁已被释放,那么协程可以立即获取锁。此时即便有协程被唤醒也无法获取锁,只能再次阻塞。
自旋的好处是,当加锁失败时不必立即转入阻塞,有一定机会获取到锁,这样可以避免协程的切换。
前面分析加锁和解锁过程中只关注了Waiter和Locked位的变化,现在我们看一下Starving位的作用。
每个Mutex都有两个模式,称为Normal和Starving。下面分别说明这两个模式。
默认情况下,Mutex的模式为normal。
该模式下,协程如果加锁不成功不会立即转入阻塞排队,而是判断是否满足自旋的条件,如果满足则会启动自旋过程,尝试抢锁。
自旋过程中能抢到锁,一定意味着同一时刻有协程释放了锁,我们知道释放锁时如果发现有阻塞等待的协程,还会释放一个信号量来唤醒一个等待协程,被唤醒的协程得到CPU后开始运行,此时发现锁已被抢占了,自己只好再次阻塞,不过阻塞前会判断自上次阻塞到本次阻塞经过了多长时间,如果超过1ms的话,会将Mutex标记为”饥饿”模式,然后再阻塞。
处于饥饿模式下,不会启动自旋过程,也即一旦有协程释放了锁,那么一定会唤醒协程,被唤醒的协程将会成功获取锁,同时也会把等待计数减1。
Woken状态用于加锁和解锁过程的通信,举个例子,同一时刻,两个协程一个在加锁,一个在解锁,在加锁的协程可能在自旋过程中,此时把Woken标记为1,用于通知解锁协程不必释放信号量了,好比在说:你只管解锁好了,不必释放信号量,我马上就拿到锁了。
type RWMutex struct {
w Mutex //用于控制多个写锁,获得写锁首先要获取该锁,如果有一个写锁在进行,那么再到来的写锁将会阻塞于此
writerSem uint32 //写阻塞等待的信号量,最后一个读者释放锁时会释放信号量
readerSem uint32 //读阻塞的协程等待的信号量,持有写锁的协程释放锁后会释放信号量
readerCount int32 //记录读者个数
readerWait int32 //记录写阻塞时读者个数
}
写锁定
解除写锁
读锁定
解除读锁定
把defer编译之后, 大体分两步, 先注册函数, 最后在return之前调用
在一个协程下注册的defer, 会被链到runtime.g 的 _defer链表里, 新注册的在头部插入, 执行的时候从头部执行
panic触发时, 也会链在runtime.g下的panic链表, 只不不过输出异常信息的时候, 是从后往前输出
panic之后会触发defer函数, 之后return
会将当前的panic的recovered字段置为true
用来控制呈树结构的goroutine的, 让每个goroutine拥有相同的上下文
提供了一个Context接口
和四种实现
emptyCtx
timerCtx
cancelCtx
valueCtx
emptyCtx用Backround和TODO函数获得
timerCtx用withTimeOut和withDeadline获得
cancelCtx用withCancel获得
valueCtx用withValue获得
M必须拥有P才可以执行G中的代码,P含有一个包含多个G的队列,P可以调度G交由M执行。
另一种情况是P所分配的任务G很快就执行完了(分配不均),这就导致了这个处理器P很忙,但是其他的P还有任务,此时如果global runqueue没有任务G了,那么P不得不从其他的P里拿一些G来执行。一般来说,如果P从其他的P那里要拿任务的话,一般就拿run queue的一半,这就确保了每个OS线程都能充分的使用,如下图:
造成引用对象丢失的条件:
一个黑色的节点A新增了指向白色节点C的引用,并且白色节点C没有除了A之外的其他灰色节点的引用,或者存在但是在GC过程中被删除了。以上两个条件需要同时满足:满足条件1时说明节点A已扫描完毕,A指向C的引用无法再被扫描到;满足条件2时说明白色节点C无其他灰色节点的引用了,即扫描结束后会被忽略 。
写屏障破坏两个条件其一即可
满足强三色不变性:黑色节点不允许引用白色节点 当黑色节点新增了白色节点的引用时,将对应的白色节点改为灰色
满足弱三色不变性:黑色节点允许引用白色节点,但是该白色节点有其他灰色节点间接的引用(确保不会被遗漏) 当白色节点被删除了一个引用时,悲观地认为它一定会被一个黑色节点新增引用,所以将它置为灰色