【golang源码分析】chan底层原理——附带读写用户队列的环形缓冲区

本文详细探讨了Go语言中Channel的工作原理,包括环形缓冲区结构、读写机制及内部数据结构。阐述了有缓冲与无缓冲Channel的区别,以及关键源码分析,帮助读者深入理解Go并发模型。

1 环形缓冲区

1.1 环形缓冲区结构

       环形缓冲区通常有一个读指针和一个写指针。读指针指向环形缓冲区中可读的数据,写指针指向环形缓冲区中可写的缓冲区。通过移动读指针和写指针就可以实现缓冲区的数据读取和写入。在通常情况下,环形缓冲区的读用户仅仅会影响读指针,而写用户仅仅会影响写指针。如果仅仅有一个读用户和一个写用户,那么不需要添加互斥保护机制就可以保证数据的正确性。如果有多个读写用户访问环形缓冲区,那么必须添加互斥保护机制来确保多个用户互斥访问环形缓冲区。

1.2 环形缓冲区一种读写实现机制

一般的,圆形缓冲区需要4个指针

  • 在内存中实际开始位置;

  • 在内存中实际结束位置,也可以用缓冲区长度代替;

  • 存储在缓冲区中的有效数据的开始位置(读指针);

  • 存储在缓冲区中的有效数据的结尾位置(写指针)。

缓冲区是满、或是空,都有可能出现读指针与写指针指向同一位置:

缓冲区中总是有一个存储单元保持未使用状态。缓冲区最多存入(size-1)个数据。如果读写指针指向同一位置,则缓冲区为空。如果写指针位于读指针的相邻后一个位置,则缓冲区为满。

 

 

 

2 chan内部数据结构

2.1 chan的数据结构

chan实质是个环形缓冲区,外加一个接受者协程队列和一个发送者协程队列

  • buf      :环形缓冲区
  • sendx :用于记录buf这个循环链表中的发送的index
  • recvx  :用于记录buf这个循环链表中接收的index
  • recvq  :接受者协程队列
  • sendq :发送者协程队列
  • lock    :互斥锁

2.2 有缓冲区和无缓冲区chan的区别

2.2.1 无缓冲chan数据同步过程和sudog结构

  1. 创建一个发送者列表和接收者列表都为空的 channel。
  2. 第一个协程向 channel 发送变量的值
  3. channel 从池中获取一个sudog结构体变量,用于表示发送者。sudog 结构体会保持对发送者所在协程的引用,以及发送变量的引用。
  4. 发送者加入sendq队列。<
### Golang 中 MySQL 查询执行时间和性能监控 在 Golang 中查询 MySQL 的执行时间以及进行性能监控是一个常见的需求。以下是关于如何实现这一目标的具体说明。 #### 使用 `context` 控制 Goroutine 生命周期并设置超时 通过 Go 提供的 `context.Context` 接口,可以在发起数据库操作前设定超时时间或其他控制条件。这有助于防止长时间运行的任务阻塞资源[^4]。例如: ```go ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() // 执行带上下文的 SQL 查询 rows, err := db.QueryContext(ctx, "SELECT * FROM users WHERE id=?", userId) if err != nil { log.Fatalf("Query failed: %v", err) } defer rows.Close() ``` 上述代码片段展示了如何利用 `context.WithTimeout` 方法为查询指定最大允许的时间间隔(此处设为两秒)。如果超过该时限,则会自动终止此操作,并释放相关联的资源。 #### 流式读取优化内存消耗 对于大规模数据集而言,采用流式的读写方式能够显著减少内存压力。这种方式仅需加载部分记录到内存即可完成处理工作,而不是试图一次性载入整个结果集合[^1]。下面是一段示范性的代码: ```go func streamRead(db *sql.DB) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() rows, err := db.QueryContext(ctx, "SELECT ...") if err != nil { return fmt.Errorf("failed to query data: %w", err) } defer rows.Close() var buffer []MyStruct // 定义缓冲区大小适中的切片用于暂存解析后的结构体实例 for rows.Next() { // 遍历每一行返回的数据项 var record MyStruct if err := rows.Scan(&record.FieldA, &record.FieldB); err != nil { return fmt.Errorf("scan row failed: %w", err) } buffer = append(buffer[:cap(buffer)], record) // 当达到一定数量后立即发送至下游通道继续后续逻辑... if len(buffer)>=batchSize{ sendBatchToChannel(itemChan,bufffer[:len(buffer)]) buffer=buffer[:0] } } return nil } ``` 这里实现了基于批量提交机制的大规模数据提取过程,在每次累积满一批次之后立即将其推送出去以便进一步加工或存储,从而维持较低水平的整体内存占用率。 #### 并发处理提升效率 为了充分利用多核 CPU 架构带来的优势,可以通过启动多个 worker goroutines 来加速任务进度。如下所示的例子中启用了三十个工作协程共同承担起接收来自 channel 的待办事项列表并且独立计算最终成果再反馈回去给调用方[^2]: ```go func startWorker(workerCount int, input chan interface{}, output chan ResultType){ wg:=sync.WaitGroup{} wg.Add(workerCount) for i:=0;i<workerCount;i++{ go func(){ defer wg.Done() for task:=range input{ res:=processTask(task.(SpecificInput)) select{ case output<-res: continue default://考虑背压情况下的限流策略 time.Sleep(time.Millisecond*50) } } }() } go func(){ wg.Wait() close(output) }() } itemChan := make(chan SpecificInput ,initialBufferSize ) resultChan :=make(chan ResultType,capacityForResults ) startWorker(30,itemChan,resultChan) close(itemChan)//关闭输入源通知所有工作者停止作业 for r:= range resultChan{...}//收集汇总各个子线程产生的中间产物直至全部结束为止 ``` 以上程序框架定义了一个简单的生产者-消费者模型,其中 producer 将原始资料放入共享队列里头等待 consumer pickup 处理完毕后再交还成品。值得注意的是还需要额外安排专门负责监听整体活动状况何时彻底完结的人手及时清理现场以免造成死锁现象的发生。 #### 内存泄漏预防措施 最后不得不提到的一点就是有关于潜在存在的goroutine泄露风险问题。由于Go runtime本身并不会主动回收那些处于无限循环状态下的轻量级线程对象因此一旦出现设计缺陷就可能导致严重的后果比如不断增长的工作池数目进而引发 OOM 错误等等严重事故[^3]。为此建议采取以下几种手段加以规避: - **定期采样分析**:借助专业的第三方库如pprof等工具定时抓取当前进程中各组成部分所占比例变化趋势图谱便于快速定位异常波动位置; - **引入外部监督体系**:依靠云计算平台自带的功能模块或者自行开发后台守护进程按照预定频率轮询目标服务器上的关键指标数值绘制曲线图表辅助判断是否存在持续攀升迹象; - **严格遵循最佳实践指南**:始终记得显式声明退出信号确保每一个新开辟出来的分支都能找到回家的道路不至于迷失方向成为孤魂野鬼游荡世间久久无法消散... --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值