
个人学习笔记资源来自网上各位大佬一、协程1、coroutineM1一个协程阻塞从属的协程也会阻塞2、goroutine有调度器实现协程和线程的动态绑定和灵活调度栈空间动态伸缩默认2KBMN实现真正的并行二、GMP1.G0调度器代码为线程m寻找空闲的p绑定在本地队列lrp取出来goroutine执行代码2.线程M阻塞发生了什么当前g将当前p放回空闲p队列取出当前g和他一同阻塞线程m终于读取到文件后找一个p进行绑定分配内存的第一路径是通过p的本地内存缓存申请没有pm无法申请执行g的内存阻塞完了先当前g寻找原先的p进行绑定再去g0在空闲p队列寻找p失败g0将当前g释放到全局队列当前线程m休眠等待被唤醒去绑定空闲p3.GMP调度流程GGoroutineGo 协程用户态线程MMachine操作系统线程PProcessor逻辑处理器代表 CPU 核心它解决了传统线程模型中“线程切换开销大、资源浪费”问题启动Go 启动时创建GOMAXPROCS个 P默认 CPU 核心数。创建 Ggo func()→ G 被加入当前 P 的本地队列。调度M 绑定 P → 从本地队列取 G 执行。M中的G0进行的逻辑调度阻塞处理M 解绑PP 会被新 M 绑定。G 被挂起等待阻塞操作完成。只有系统调用m才阻塞否则挂起恢复阻塞完成 → G 重新加入 P 的LRQ。工作窃取P 的队列为空时M 会从其他 P 的队列“偷”任务避免空闲。顺序本地队列--全局P队列其他P-从tail开始偷一半4.P设计的好处P的存在实现了无锁的本地调度。每个P维护独立的本地队列M绑定P后可以直接从本地队列取G执行大部分情况下都不需要全局锁。只有本地队列空了才去偷取这大大减少了锁竞争。5.介绍一下GMP模型GMP模型是goroutine调度模型让成千上万的goroutine高效地运行在少量os线程上GGoroutine用户态协程初始栈2kb可动态伸缩MMachineOS 线程内核线程PProcessor逻辑处理器连接 G 和 M 的核心go func()创建一个gg 进入当前 p 的本地队列LRQ满了就进入全局队列 GRQm 绑定 p 在本地队列取 g 运行如果无 g 就 work stealing 机制从其他 p 本地队列尾部开始偷取一半没有得偷就去全局队列 GRQ 偷如果 g 阻塞m 解绑 p避免卡住cpu然后挂起等待g阻塞完毕p会被新 m 绑定执行 g通过本地队列work stealing抢占调度syscall解绑来让海量goroutine高效运行一、G 阻塞的两种核心场景及 M-P 解绑逻辑场景 1G 进入系统调用如文件 IO、sleep、网络阻塞早期G 执行到os.Open()/time.Sleep()等系统调用 → 触发 M 进入内核态阻塞关键动作M 立刻释放绑定的 PP 会被 runtime 重新放入 “空闲 P 列表”空闲 M或新建 M会从空闲列表中抢占这个 P → P 继续调度本地队列的其他 G 执行当原 G 的系统调用结束 → 这个 G 会被放入可运行队列本地 / 全局而原 M 则若能抢到空闲 P → 继续执行这个 G若抢不到 → M 会被休眠减少系统线程开销等待后续唤醒。场景 2G 因网络 IO 阻塞如net.Dial()/http.Get()这种场景更高效Go 的网络库会把 IO 事件交给NetPoller网络轮询器处理而非让 M 阻塞G 触发网络 IO → runtime 先把 G 挂到 NetPoller 的等待队列直接让 M 释放 GM 无需阻塞继续用 P 调度下一个 GM-P 不解绑当 IO 事件完成如连接建立 / 数据到达→ NetPoller 把 G 重新放回可运行队列等待 M 调度。二、为什么要这么设计核心价值你可以把 P 理解为 “CPU 核心的使用权”GOMAXPROCS 本质是限制 P 的数量 CPU 核心数如果 M 阻塞时不释放 P → 这个 P 就被 “锁死” 在阻塞的 M 上对应 CPU 核心空转其他 G 只能排队如果 M 阻塞时释放 P → P 能立刻被其他 M 复用CPU 核心始终有 G 在执行利用率拉满。举个直观例子假设你的机器是 4 核GOMAXPROCS4有 4 个 P。如果一个 G 因 sleep (10s) 阻塞若 M 不释放 P → 这 10s 内只有 3 个 P 在干活1 个 CPU 核心闲置而释放 P 后 → 4 个 P 全程都在调度 GCPU 利用率保持 100%。三、1. go程序启动发生了什么深入原理命令行参数复制读取命令行参数复制到 argc 和 argv。初始化 g0 栈g0 是运行时系统的一个特殊的 goroutine它在程序启动时被创建用于执行系统调用和协程调度。runtime.check 运行时检查类型长度指针操作结构体字段偏移量.......runtime.args 参数初始化将 argc 和 argv 的参数赋值到 Go 的变量中。runtime.osinit 初始化操作系统特点的设置主要是判断系统字长和 CPU 核数。runtime.schedinit 初始化调度器锁初始化内存分配器初始化GC初始化调度器初始......runtime.newproc 创建主协程 g0 并将其放入队列中等待执行。runtime. mstart 启动调度器初始化 m0并调度 g0 去执行2. Slice切片原理type slice struct { array unsafe.Pointer // 指向底层数组的指针 len int // 切片的有效长度当前可访问的元素个数 cap int // 切片的容量底层数组从指针位置开始的总元素个数 }赋值、传参、截取都是操作array本身自动扩容1.8以前cap 1024翻倍cap 1024新容量原容量 X 1.25深拷贝向上取整 到 内存对齐的整数倍自动扩容1.8及以后需要容量 2倍: 直接使用需要的容量cap 256翻倍2xcap256newcap oldcap (oldcap 3 * 256 ) / 4直到容量够深拷贝向上取整 到 内存对齐的整数倍3. Map映射原理type hmap struct { count int // map中实际存储的键值对数量 (len(map)返回的值) flags uint8 // 状态标识是否正在扩容、是否并发写等 B uint8 // 哈希桶的数量 2^B 个核心扩容相关字段 noverflow uint16 // 溢出桶的数量越多代表哈希冲突越严重 hash0 uint32 // 哈希种子随机值保证哈希结果的均匀性 buckets unsafe.Pointer // 指向「哈希桶数组」的指针核心存储区域 oldbuckets unsafe.Pointer // 扩容时用指向「旧哈希桶数组」的指针 nevacuate uintptr // 扩容时的搬迁进度标记 extra *mapextra // 存储溢出桶的内存地址 } type bmap struct { tophash [8]uint8 // 存储每个键的哈希值高8位用于快速查找匹配 // 后续是8个key、8个value内存地址连续存储编译期自动填充 // 最后是一个溢出桶指针指向当前桶存满后挂载的下一个bmap }Go 的map是引用类型和切片一样不是值类型赋值 / 传参时只拷贝底层结构体指针修改会互相影响Gomap的底层实现是「拉链法」哈希表核心解决哈希冲突的问题Gomap无序遍历结果的元素顺序不固定每次遍历都可能不一样Gomap非并发安全多个 goroutine 同时读写会 panicmap的键key必须满足可比较性支持/!切片、map、函数不能作为 key。插入和查找hash后共64位key低B位value高8位自动扩容装载因子 6.5桶数量翻倍溢出桶太多等量扩容桶的数量不变只是将键值对重新排列减少溢出桶的使用。bucket_cnt 2^15如果溢出桶数量超过桶数量就会触发扩容bucket_cnt 2^15时如果溢出桶数量超过2^15就会触发扩容渐进式扩容每次写 操作迁移 1-2 个桶4. Channel原理type hchan struct { qcount uint // 当前队列元素个数 dataqsiz uint // 缓冲区长度由 make 的 N 决定 buf unsafe.Pointer // 指向环形缓冲区元素大小 sizeof(T) elemsize uint16 closed uint32 // 关闭标志CAS 置 1 elemtype *_type // 元素类型信息用于 GC 扫描 sendx uint // 发送索引 recvx uint // 接收索引 //无缓冲channel只用到下面字段 recvq waitq // 等待接收的 sudog 双链表 sendq waitq // 等待发送的 sudog 双链表 lock mutex // 一把全局锁保护 hchan 内部字段 } type waitq struct { first, last *sudog }发送chansendlock判断是否关闭是则panic判断recvq有无有则直接发完整拷贝一个过去goready唤醒unlock判断是否有缓冲区qcountdataqsiz有则发送到缓冲区unlock无则初始化sudog放入sendqgopark接受chanrecvlock判断是否关闭是则return判断sendq有无有则直接接受完整拷贝一个过来goready唤醒unlock判断缓冲区是否已经有数据qcount0是则接受到缓冲区unlock无则初始化sudog放入recvqgopark为什么选用chanel来进行协会通信首先是重点go推崇“不要通过共享内存来通信而应通过通信来共享内存”。天然避免了数据竞争传统锁机制需要手动管理容易死锁遗漏channal通过数据所有权的转移保证同一时刻只有一个goroutine来操作数据简化流程数据传递和同步合二为一发送/接受数据本身包含阻塞等待不需要额外的信号量或者condition简单易读PS⽆缓冲的 channel 是同步的⽽有缓冲的 channel 是⾮同步的从⼀个 nil channel 接收数据造成永远阻塞给⼀个 nil channel 发送数据造成永远阻塞从⼀个已经关闭的 channel 接收数据如果缓冲区中为空则返回⼀个零值给⼀个已经关闭的 channel 发送数据引起 panic关闭⼀个 nil channel 将会发⽣ panic5、deferdefer 是 Go 语言中用于延迟执行函数调用的关键字。它的主要作用是确保资源释放如文件关闭、锁释放异常恢复配合 recover 处理 panic执行顺序后进先出LIFO参数求值参数在 defer 声明时立即求值返回值修改可以修改命名返回值实现原理上defer 在编译时会被转换成_defer结构体形成一个链表。函数返回时按链表顺序执行 defer。Go 1.13 对 defer 进行了性能优化支持栈分配和开放编码。//① 先计算返回值 → ② 再按 LIFO 执行 defer → ③ 最终才真正返回给调用方。 func f() (r int) { // 命名返回值 defer func() { r }() // ② 在RET之前执行成功改值 return 1 // ① 先完成 r 1 } // ③ 真正返回 26、for range// 源代码 slice : []int{1, 2, 3} for i, v : range slice { fmt.Println(i, v) } // 编译器大致转换为 for i : 0; i len(slice); i { v : slice[i] // 关键值拷贝 fmt.Println(i, v) } //所以如果需要用到指针要用i索引 slice : []int{1, 2, 3} for i, v : range slice { func(slice[i]) }7、内存逃逸内存逃逸原本应该在栈上分配的内存被分配到了堆上// 1、返回局部变量指针 → 生命周期栈帧必逃 func foo() *int { x : 42; return x } // x escapes to heap // 2、把指针扔进 channel → 编译期不知接收方只能堆上 func bar() { ch : make(chan *int) v : 1 ch - v // v escapes to heap } // 3、切片里存指针 → 切片本体可栈但指向的元素必逃 func baz() []*string { s : hi; return []*string{s} } // s escapes to heap // 4、append 导致重新分配 → 运行时才能决定新数组地址逃 func qux() []int { s : make([]int, 0, 2) // 初始栈上 s append(s, 1, 2, 3) // 超出 cap新数组 escapes to heap return s } // 5、interface 方法调用 → 动态派发接收器与实参都逃 type animal interface { run() } type dog struct {} func (a dog) run() {} func main() { //编译时就知道类型栈 var a animal dog{} a.run() //不知道类型堆 var a1 animal a1dog{} a1.run() } // 6. 大块对象 64 KB func bigHeap() { b : make([]byte, 120) // 1 MB b[0] 1 fmt.Printf(big slice len %d\n, len(b)) }简而言之局部变量的生命周期超出了所在函数的生命周期变量类型不确定变量大小不确定或者太大为什么要避免内存逃逸大量的逃逸会影响应用程序性能8、CSP模型核心思想不要通过共享内存来通信而应通过通信来共享内存。避免竞态条件简化同步不需要显式上锁解锁通道自动同步特性CSP通道通信共享变量通信通信方式通过通道发送和接受消息通过共享内存读写数据数据共享数据通过消息传递不直接共享内存数据存储在共享内存同步机制通道的发送和接受操作自动同步需要显示使用锁信号量等同步机制编程复杂度更简单避免竞态条件更复杂手动管理典型语言GoC、C、Java性能通道通信存在一定开销直接访问内存性能较高使用场景高并发、任务间通信频繁的场景需要精细控制内存访问的场景9、GO GC垃圾回收三色标记法1、开始全部对象都在白色集合2、然后把根节点移到灰色集合3、循环遍历灰色集合把其引用对象不断加入灰色集合然后自己去黑色集合4、直到灰色集合为空完整的 GC 周期分为 4 个阶段其中仅 2 个阶段有极短的 STWSTW - 初始标记Initial Mark暂停所有 goroutine标记根对象栈、全局变量耗时极短百微秒级并发标记Concurrent Mark恢复 goroutineGC 后台线程并发遍历堆中对象标记存活对象此阶段若程序修改对象引用如指针赋值写屏障会记录修改确保标记不遗漏若程序内存分配速率过快会触发 “辅助 GC”让分配内存的 goroutine 协助做标记工作避免 GC 滞后。STW - 最终标记Final Mark再次短暂暂停 goroutine处理剩余未标记的少量对象如写屏障记录的修改并发清理Concurrent Sweep恢复 goroutineGC 后台线程并发清理未标记的死亡对象将内存归还给堆或操作系统。请你介绍一下go的gc垃圾回收混合屏障只要堆上指针修改同时标记新旧对象Go的gc采用的是三色回收法主要分4个阶段STW-初始标记暂停goroutine开启写屏障标记根节点加入灰色集合并发标记恢复goroutine并发循环遍历灰色集合把引用的对象加入灰色集合自己进入黑色集合有写屏障会处理并发对象STW-最终标记暂停goroutine处理剩下未标记的对象写屏障的修改并发清理恢复goroutine并发清理未标记的对象为什么混合屏障跳过栈扫描采用栈全黑提高性能无需快照减少内存开销10、go的优势1、极致的并发性能go的并发模型cspgoroutine初始栈2KB一台机器就能百万级goroutinechannel2、语法简洁只有25个关键字快速上手3、高性能编译性语言执行效率高gc垃圾回收stw停顿时间短微秒级4、标准库非常丰富