channel主要用于进程内各goroutine间通信,如果需要跨进程通信,建议使用分布式系统的方法来解决
关闭channel时会把recvq中的G全部唤醒,本该写入G的数据位置为nil。把sendq中的G全部唤醒,但这些G会panic。
除此之外,panic出现的常见场景还有:
依托数组实现
数据结构
type slice struct {
array unsafe.Pointer 指针,array指针指向底层数组
len int 长度
cap int 容量
}
使用make来创建Slice时,可以同时指定长度和容量,创建时底层会分配一个数组,数组的长度即容量。
使用数组来创建Slice时,Slice将与原数组共用一部分内存
slice扩容
扩容容量的选择遵循以下规则:
使用append()向Slice添加一个元素的实现步骤如下:
type hmap struct {
count int // 当前保存的元素个数
...
B uint8
...
buckets unsafe.Pointer // bucket数组指针,数组的大小为2^B
...
}
bucket数据结构
type bmap struct {
tophash [8]uint8 //存储哈希值的高8位
data byte[1] //key value数据:key/key/key/.../value/value/value...
overflow *bmap //溢出bucket的地址
}
上述中data和overflow并不是在结构体中显示定义的,而是直接通过指针运算进行访问的。
根据key(键)即经过一个函数f(key)得到的结果的作为地址去存放当前的key value键值对(这个是hashmap的存值方式),但是却发现算出来的地址上已经被占用了。这就是所谓的*hash冲突*
使用溢出桶解决hash冲突
负载因子 = 键数量/bucket数量
条件
增量扩容和等量扩容的区别
增量扩容是当容量不够时发生的,会在内存中开辟一块新的空间,是原空间的两倍,然后将数据搬迁到新的桶中,
等量扩容是容量不变,发生在键值对不集中,操作和增量扩容一致,但是容量不变,会将里面的简直对重新排列
主要了解tag,主要用于反射
iota代表了const声明块的行索引(下标从0开始)
规则
1、延迟函数的参数在defer语句出现时就已经确定下来了
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
defer语句中的fmt.Println()参数i值在defer出现时就已经确定下来,实际上是拷贝了一份。后面对变量i的修改不会影响fmt.Println()函数的执行,仍然打印”0”。
注意:对于指针类型参数,规则仍然适用,只不过延迟函数的参数是一个地址值,这种情况下,defer后面的语句对变量的修改可能会影响延迟函数。
2、延迟函数执行按后进先出顺序执行,即先出现的defer最后执行
定义defer类似于入栈操作,执行defer类似于出栈操作。
设计defer的初衷是简化函数返回时资源清理的动作,资源往往有依赖顺序,比如先申请A资源,再根据A资源申请B资源,根据B资源申请C资源,即申请顺序是:A–>B–>C,释放时往往又要反向进行。这就是把defer设计成LIFO的原因。
3、延迟函数可能操作主函数的具名返回值
return分两步,即将返回值存入栈中做返回值。然后执行跳转,而defer执行时机是在跳转前
nil
;(一般panic语句如panic("xxx failed...")
)遍历切片
遍历slice前会先获取slice的长度len_temp作为循环次数,循环体中,每次循环会先获取元素值,如果for-range中接收index和value的话,则会对index和value进行一次赋值。
由于循环开始前循环次数就已经确定了,所以循环过程中新添加的元素是没办法遍历到的。
另外,数组与数组指针的遍历过程与slice基本一致
map遍历
遍历map时没有指定循环次数,循环体与遍历slice类似。由于map底层实现与slice不同,map底层使用hash表实现,插入数据位置是随机的,所以遍历过程中新插入的数据不能保证遍历到。
channel遍历
channel遍历是依次从channel中读取数据,读取前是不知道里面有多少个元素的。如果channel中没有元素,则会阻塞等待,如果channel已被关闭,则会解除阻塞并退出循环。
结构体
type Mutex struct {
state int32 //表示互斥锁的状态,比如是否被锁定
sema uint32 //表示信号量,协程阻塞等待该信号量,解锁的协程释放信号量从而唤醒等待信号量的协程。
}
四种状态
加锁后立即使用defer对其解锁,可以有效的避免死锁。
提高并发能力
读锁不阻塞读锁、其他都会造成阻塞
type RWMutex struct {
w Mutex //用于控制多个写锁,获得写锁首先要获取该锁,如果有一个写锁在进行,那么再到来的写锁将会阻塞于此
writerSem uint32 //写阻塞等待的信号量,最后一个读者释放锁时会释放信号量
readerSem uint32 //读阻塞的协程等待的信号量,持有写锁的协程释放锁后会释放信号量
readerCount int32 //记录读者个数
readerWait int32 //记录写阻塞时读者个数
}
M必须拥有P才可以执行G中的代码,P含有一个包含多个G的队列,P可以调度G交由M执行
图中M是交给操作系统调度的线程,M持有一个P,P将G调度进M中执行。P同时还维护着一个包含G的队列(图中灰色部分),可以按照一定的策略将G调度到M中执行。
P的个数在程序启动时决定,默认情况下等同于CPU的核数,由于M必须持有一个P才可以运行Go代码,所以同时运行的M个数,也即线程数一般等同于CPU的个数,以达到尽可能的使用CPU而又不至于产生过多的线程切换开销。
每个P维护着一个包含G的队列,不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队列中重新取出一个G进行调度。
除了每个P维护的G队列以外,还有一个全局的队列,每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G。之所以P会周期性地查看全局队列,也是为了防止全局队列中的G被饿死。
在Golang程序启动时会向系统申请一块内存
Golang内存分配是个相当复杂的过程,其中还掺杂了GC的处理,这里仅仅对其关键数据结构进行了说明,了解其原理而又不至于深陷实现细节。
垃圾回收的核心就是标记出哪些内存还在使用中(即被引用到),哪些内存不再使用了(即未被引用),把未被引用的内存回收掉,以供后续内存分配时使用。
垃圾回收触发时机
每次内存分配时都会检查当前内存分配量是否已达到阀值,如果达到阀值则立即启动GC。
阀值 = 上次GC内存分配量 * 内存增长率
内存增长率由环境变量GOGC
控制,默认为100,即每当内存扩大一倍时启动GC。
初始状态下所有对象都是白色的。
接着开始扫描根对象a、b; 由于根对象引用了对象A、B,那么A、B变为灰色对象,接下来就开始分析灰色对象,分析A时,A没有引用其他对象很快就转入黑色,B引用了D,则B转入黑色的同时还需要将D转为灰色,进行接下来的分析。
灰色对象只有D,由于D没有引用其他对象,所以D转入黑色。标记过程结束
最终,黑色的对象会被保留下来,白色对象会被回收掉。
程序代码中也可以使用runtime.GC()
来手动触发GC。这主要用于GC性能测试和统计。
Stack scan:Collect pointers from globals and goroutine stacks。收集根对象(全局变量,和G stack),开启写屏障。全局变量、开启写屏障需要STW,G stack只需要停止该G就好,时间比较少。
Mark: Mark objects and follow pointers。标记所有根对象, 和根对象可以到达的所有对象不被回收。
Mark Termination: Rescan globals/changed stack, finish mark。重新扫描全局变量,和上一轮改变的stack(写屏障),完成标记工作。这个过程需要STW。
Sweep: 按标记结果清扫span
栈上分配的内存不需要GC处理
就是本该在栈中的现在在堆中
场景
1、指针逃逸
2、栈空间不足逃逸
栈空间不足以存放当前对象时或无法判断当前切片长度时会将对象分配到堆中
3、动态类型逃逸
参数在编译阶段难以确定其类型,会产生逃逸
4、闭包引用对象逃逸
channel一般用于协程之间的通信,channel也可以用于并发控制。比如主协程启动N个子协程,主协程等待所有子协程退出后再继续后续流程
WaitGroup是Golang应用开发过程中经常使用的并发控制技术。
等待一组协程结束后,某个协程才可继续的功能
Add()做了两件事,一是把delta值累加到counter中,因为delta可以为负值,也就是说counter有可能变成0或负值,所以第二件事就是当counter值变为0时,根据waiter数值释放等量的信号量,把等待的goroutine全部唤醒,如果counter变为负值,则panic.
它与WaitGroup最大的不同点是context对于派生goroutine有更强的控制力,它可以控制多级的goroutine。
Timer实际上是一种单一事件的定时器,即经过指定的时间后触发一个事件,这个事件通过其本身提供的channel进行通知。之所以叫单一事件,是因为Timer只执行一次就结束,这也是Timer与Ticker的最重要的区别之一。
设定超时时间
延迟执行某个方法
type Timer struct {
C <-chan Time
r runtimeTimer
}
Ticker是周期性定时器,即周期性的触发一个事件,通过Ticker本身提供的管道将事件传递出去。
Ticker的数据结构与Timer完全一致: