1714 字
9 分钟
go学习
感觉我学习语言的方式不对
我只是简单的在用语言都有的功能进行开发, 但是没有去了解语言的很多特性
虽然可能会有点功利,但是是不是对面试不太好🥲🥲
goroutine 协程的底层原理
原来的线程:由内核创建,由操作系统分配cpu时间片的最小单位
一个线程要有用户栈,内核栈,TCB(线程控制块,保存线程状态:阻塞、运行、就绪,优先级,CPU上下文的数据结构),通常1mb
内核栈保存寄存器状态
sync.Mutex 互斥锁,就是锁 sync.RWMutex 读写锁,读可并发,写不行
import ( mu sync.Mutex mu2 sync.RWMutex wp sync.WaitGroup
)func main(){ wp.Add(1) go func(){ defer wp.Done() log.Println("1") }
mu.Lock() // 互斥锁 mu2.Lock() // 写锁 mu2.RLock() // 读锁
mu.UnLock() // 互斥锁 mu2.UnLock() // 写锁 mu2.RUnLock() // 读锁
wp.Wait()
}channel:用通信的方式共享内存,而不是用共享的内存来通信
go context底层原理
context的底层设计,我们可以概括为1个接口,4种实现与6个方法。
-
1 个接口
- Context 规定了
context的四个基本方法
- Context 规定了
-
4 种实现
- emptyCtx 实现了一个空的
context,可以用作根节点 - cancelCtx 实现一个带
cancel功能的context,可以主动取消 - timerCtx 实现一个通过定时器
timer和截止时间deadline定时取消的context - valueCtx 实现一个可以通过
key、val两个字段来存数据的context
- emptyCtx 实现了一个空的
-
6 个方法
- Background 返回一个
emptyCtx作为根节点 - TODO 返回一个
emptyCtx作为未知节点 - WithCancel 返回一个
cancelCtx - WithDeadline 返回一个
timerCtx - WithTimeout 返回一个
timerCtx - WithValue 返回一个
valueCtx
- Background 返回一个
chennel源码:
type hchan struct { qcount uint // 队列中现存的元素总数 dataqsiz uint // 环形队列的容量(即缓冲区大小) buf unsafe.Pointer // 指向底层连续内存数组的指针(存放数据的地方) elemsize uint16 // 元素的大小(字节数,比如 int64 就是 8 字节) closed uint32 // 标识通道是否关闭(0: 打开, 1: 关闭) elemtype *_type // 元素类型(用于反射和类型安全检查)
sendx uint // 缓冲区发送索引(数组下标,下次写数据的位置) recvx uint // 缓冲区接收索引(数组下标,下次读数据的位置)
recvq waitq // 阻塞在这里等待【接收】数据的协程链表(sudog 链表) sendq waitq // 阻塞在这里等待【发送】数据的协程链表(sudog 链表)
lock mutex // 互斥锁!保护上面所有字段的并发安全}
func chansend(c *hchan, ep unsafe.Pointer, block bool) bool { // 1. 如果 channel 是空的(nil) if c == nil { if !block { return false } // 如果是阻塞发送,直接让当前协程永久躺倒(挂起) gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoBlockSend, 2) throw("unreachable") }
// 2. 核心操作开始:第一步永远是【加锁】 lock(&c.lock)
// 3. 如果 channel 已经关闭,加锁状态下抛出 panic if c.closed != 0 { unlock(&c.lock) panic(plainError("send on closed channel")) }
// 4. 【情况一:有现成的接收者在等数据】(无锁零拷贝优化) // 从 recvq 队列里弹出一个正在死等数据的协程(sudog) if sg := c.recvq.dequeue(); sg != nil { // 找到了等待的接收者!直接把数据拷贝到它的栈内存里,并唤醒它 // 这个过程不需要经过 c.buf 缓冲区中转 send(c, sg, ep, func() { unlock(&c.lock) }, 3) return true }
// 5. 【情况二:缓冲区还没满】 if c.qcount < c.dataqsiz { // 还有空位,把数据放进环形数组 c.buf qp := chanbuf(c, c.sendx) // 找到当前发送索引对应的内存地址 typedmemmove(c.elemtype, qp, ep) // 将数据 ep 拷贝到 qp 指向的缓冲区 c.sendx++ // 发送下标往后移 if c.sendx == c.dataqsiz { // 环形队列走到末尾,折返回头部 c.sendx = 0 } c.qcount++ // 队列元素计数 +1
unlock(&c.lock) // 【解锁】,发送成功返回 return true }
// 如果是异步非阻塞发送(例如 select 里的 send),缓冲区满了就直接解锁返回 false if !block { unlock(&c.lock) return false }
// 6. 【情况三:缓冲区满了,且无人在等数据】(协程挂起) gp := getg() // 获取当前的协程对象(Goroutine) mysg := acquireSudog() // 从对象池里打包一个临时的登记节点(sudog) mysg.elem = ep // 把你要发送的数据地址挂在节点上 mysg.g = gp // 节点绑定当前协程 mysg.c = c // 节点绑定当前 channel
c.sendq.enqueue(mysg) // 把这个节点推进 channel 的发送等待链表
// 【关键动作】:调用 gopark 释放锁,并将当前协程转入等待状态 // 从这一刻起,当前协程不占用 CPU,直到别人读取数据将其唤醒 gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
// ———————— 被唤醒后的分割线 ———————— // 当另一个协程读取了数据,它会调用 goready() 重新激活这个协程,代码从这里继续往下走
releaseSudog(mysg) // 释放临时登记节点 return true}channel源码总结 chennel send: 1.消息进入之后,先判断channel是否存在 (hcan struct) 如果没有的话,判断是否是异步非阻塞 如果不是,直接返回,没有channel也就不需要进行逻辑了 是,则将该协程永久挂起(这里如果实在main里面使用无缓冲channel,会将main协程 (是的,main也是协程)直接挂起,由于现在main协程都没了,runtime轮询查找可用的协程,但是找不到,只剩堆里面一个channel在运作,直接判断死锁) 之后先加锁 如果channel关闭了,直接panic 如果有订阅者,就把消息发给这个人(FIFO 先到先得) 没有订阅者,缓冲区还没满,写入缓冲区 缓冲区满了,挂起当前协程
// 运行时的底层判定逻辑(伪代码) func checkdead() { run := sched.runnablecnt // 当前可运行的协程数量 if run > 0 { return // 还有人能跑,不算死锁 } // 关键账目对比: // nmidlelocked 是被锁住的线程,sysmon 是系统监控 // 除去这些后,看看全天下总共还有多少个活着的协程(包括睡着的) aliveCount := runtime.gcount() if aliveCount == 0 { // 如果连睡着的协程都没有,说明程序是正常运行完自然退出的 return } // 【致命判定】:如果活着的协程数量 > 0(比如 main 还在),但可运行的数量为 0 // 这意味着:这群活着的协程全部在睡觉,且断绝了所有被唤醒的可能! panic("fatal error: all goroutines are asleep - deadlock!") }同步阻塞:
ch := make(chan int)同步非阻塞:
select{case data <- ch: printlf("发送成功")default: println("干一些别的事情")}异步非阻塞: 网络IO调用,我在等待数据的时候,把使用数据的回调函数注册,数据到了自动调用留下的回调函数
随学随写了(