Go中的协程并发和并发panic处理
1 协程基础
1.1 协程定义(Goroutine)
- 概念:Go 语言特有的轻量级线程,由 Go 运行时(runtime)管理,相比系统线程(Thread),创建和销毁成本极低,占用内存小(初始 2KB)。协程是 Go 程序中最基本的并发执行单元。
-
创建方式:使用
go
关键字启动一个协程func main() {// 匿名函数直接启动协程go func() {fmt.Println("Hello from goroutine!")}() // 调用已定义函数启动协程go func1()go func2()time.Sleep(time.Second) // 等待协程执行,否则主协程退出导致所有协程终止fmt.Println("主协程退出") }
1.2 协程调度模型GMP
Go 调度器采用 Goroutine-Machine-Processor (GMP) 模型,核心组件包括:
- G (Goroutine):协程的抽象,包含执行栈、程序计数器等信息。
- M (Machine): 对应操作系统线程,实际执行代码的实体。
- P (Processor):逻辑处理器,持有运行队列(Local Queue)和 G 上下文,必须绑定 M 才能执行 G。
M 是 “执行任务的实体”,是唯一能运行 Go 代码的载体。G(任务)本身只是一段代码逻辑,必须依赖 M(操作系统线程)才能在 CPU 上执行
M和P是绑定关系,必须成对出现
1.2.1 协程创建
- 当调用
go func()
时,创建一个新的 G 对象,放入当前 P 的 Local Queue。 - 若 Local Queue 已满(默认 256 个 G),将一半 G 转移到全局队列(Global Queue)。
1.2.2 协程执行
- M 从绑定的 P 的 Local Queue 获取 G 执行。
- 若 Local Queue 为空,从 Global Queue 批量获取 G(通常为 P 的 GOMAXPROCS/2)。
- 若 Global Queue 也为空,从其他 P 的 Local Queue 窃取(Work Stealing) 一半 G。
1.2.3 协程阻塞 / 唤醒
- 当 G 执行系统调用(如 I/O)时,M 与 P 解绑,P 可被其他 M 接管继续执行队列中的 G。
如果 M 因系统调用被阻塞时,P 继续绑定 M,会导致以下问题:
- P 无法工作:P 的本地队列中可能有大量就绪的 G,但由于 M 被阻塞,这些 G 无法执行。
- CPU 核心浪费:如果 P 对应一个 CPU 核心,该核心将处于闲置状态,即使还有其他任务可执行。
- 因此,当 G 执行系统调用时,调度器会 解绑 M 和 P,允许 P 继续工作,避免 CPU 资源浪费
- 系统调用返回后,G 重新加入某个 P 的队列等待执行。
2 并发模式
2.1 共享内存并发
多个协程通过共享变量访问数据,需使用同步原语(如sync.Mutex
、sync.RWMutex
)保护临界区
var (counter intmu sync.Mutex
)func increment() {mu.Lock()defer mu.Unlock()counter++
}func main() {var wg sync.WaitGroupfor i := 0; i < 1000; i++ {wg.Add(1)go func() {defer wg.Done()increment()}()}wg.Wait()fmt.Println("Counter:", counter) // 输出1000,无竞争
}
2.2 CSP 并发(通过通道通信)
使用channel
实现协程间通信和同步,遵循 “不要通过共享内存来通信,而要通过通信来共享内存” 原则
func producer(ch chan<- int) {for i := 0; i < 5; i++ {ch <- i}close(ch)
}func consumer(ch <-chan int) {for num := range ch {fmt.Println("Received:", num)}
}func main() {ch := make(chan int)go producer(ch)consumer(ch)
}
2.3 并发任务控制
// 普通的协程创建方法:
go func() {// your code1
}()
go func() {// your code2
}()
// go on
这段 Go 代码的执行顺序如下:
-
启动 goroutine 1:主协程创建并启动第一个匿名函数(
// your code1
),该函数在后台异步执行。 -
启动 goroutine 2:主协程紧接着创建并启动第二个匿名函数(
// your code2
),同样在后台异步执行。 -
主协程继续执行:主协程不会等待这两个 goroutine 完成,而是立即继续执行
// go on
之后的代码。 -
并行执行 goroutine:
// your code1
和// your code2
的执行顺序取决于调度器,可能并行或交替执行,但它们的完成顺序不确定。由于主协程未等待它们,若主协程提前结束(例如程序退出),这两个 goroutine 可能被强制终止。
2.3.1 sync.WaitGroup
wg.Wait()会阻塞直到2个协程执行完后
go func() {
// func1wg.Done()
}()
go func() {
// func2wg.Done()
}()
wg.Wait()
// go on
这段 Go 代码的执行顺序如下:
-
启动 goroutine 1:主协程创建并启动第一个匿名函数(
func1
),该函数在后台异步执行。 -
启动 goroutine 2:主协程紧接着创建并启动第二个匿名函数(
func2
),同样在后台异步执行。 -
主协程阻塞:主协程执行
wg.Wait()
,进入阻塞状态,等待所有被等待的 goroutine 完成。 -
并行执行 goroutine:
func1
和func2
的执行顺序取决于调度器,可能并行或交替执行,但它们的完成顺序不确定。每个 goroutine 在完成任务后调用wg.Done()
通知等待组。 -
恢复主协程:当所有被等待的 goroutine(即
func1
和func2
)都调用了wg.Done()
后,wg.Wait()
返回,主协程继续执行后续代码(// go on
)。
主协程 | goroutine 1 | goroutine 2
---------------------------------------------------------------
wg.Add(2) | |
启动func1 | 开始执行func1 |
启动func2 | | 开始执行func2
wg.Wait()阻塞 | ... | ...
| 执行完毕 |
| wg.Done() |
| | 执行完毕
| | wg.Done()
wg.Wait()返回 | |
继续执行后续代码
2.3.2 errgroup.Group
var g errgroup.Group
g.Go(func() error {// 任务1:可能返回错误return nil
})
g.Go(func() error {// 任务2:可能返回错误return errors.New("task failed")
})
if err := g.Wait(); err != nil {// 处理首个错误(如任务2失败)
}
执行顺序:
-
主协程启动两个 goroutine 并行执行
-
若其中一个 goroutine 返回非 nil 错误:
-
自动调用内置的
context.CancelFunc
-
向其他 goroutine 发送取消信号(通过 context)
-
g.Wait()
立即返回首个错误
-
-
所有 goroutine(包括未出错的)需主动检查 context 状态并提前退出
2.3.3 对比
特性 | errgroup.Group | sync.WaitGroup |
---|---|---|
错误处理 | 自动捕获首个非 nil 错误并终止所有 goroutine | 不处理错误 |
执行控制 | 首个错误发生后,其他 goroutine 会被 CancelFunc 终止 | 所有 goroutine 独立运行至完成 |
结果聚合 | 可返回首个错误,用于统一错误处理 | 无内置错误传递机制 |
取消机制 | 支持通过 context 传播取消信号 | 无内置取消机制 |
3. 并发panic处理
协程中发生 panic 若未被捕获,仅会导致该协程崩溃,不会影响其他协程和主程序,但可能导致资源泄漏
3.1 普通goroutine的panic处理
对于普通的goroutine,可以在协程函数内部使用defer和recover组合来捕获panic。defer语句会将函数推迟到外层函数返回之前执行,而recover函数用于捕获panic,它只能在defer修饰的函数中有效
func worker() {defer func() {if r := recover(); r != nil {fmt.Println("Recovered in worker:", r)}}()// 可能触发panic的代码var data map[string]intdata["key"] = 1 // 触发panic: assignment to entry in nil map
}func main() {go worker()time.Sleep(time.Second)fmt.Println("Main continues")
}
3.2 使用 sync.WaitGroup 时的 panic 处理
sync.WaitGroup常用于等待一组goroutine完成任务。在这种场景下,每个goroutine内部仍需使用defer和recover捕获panic,并且可以通过额外的机制将panic信息传递给主协程。
import ("fmt""sync"
)type Result struct {Err errorData interface{}
}func worker(id int, wg *sync.WaitGroup, resultChan chan<- Result) {defer func() {if r := recover(); r != nil {resultChan <- Result{Err: fmt.Errorf("panic in worker %d: %v", id, r)}}}()// 模拟可能触发panic的任务if id == 2 {panic("simulated panic")}resultChan <- Result{Data: fmt.Sprintf("Worker %d finished", id)}wg.Done()
}func main() {var wg sync.WaitGroupresultChan := make(chan Result)numWorkers := 3for i := 1; i <= numWorkers; i++ {wg.Add(1)go worker(i, &wg, resultChan)}go func() {wg.Wait()close(resultChan)}()for result := range resultChan {if result.Err != nil {fmt.Println(result.Err)} else {fmt.Println(result.Data)}}
}
worker函数通过defer和recover捕获panic,并将错误信息封装成Result结构体发送到resultChan通道。主协程从通道中接收结果,判断是否存在错误并进行相应处理,确保即使有goroutine发生panic,也能及时获取信息并继续执行后续逻辑。
3.3 使用 errgroup.Group 时的 panic 处理
errgroup.Group可以方便地并行执行多个任务,并在其中一个任务出错时快速返回错误。然而,它只能处理函数返回的错误,无法自动捕获goroutine内部的panic。因此,需要手动在每个任务函数中添加panic捕获逻辑,并将panic转换为错误返回给errgroup.Group。
3.3.1 方法一:手动封装panic捕获
import ("fmt""golang.org/x/sync/errgroup"
)func safeGo(g *errgroup.Group, fn func() error) {g.Go(func() error {defer func() {if r := recover(); r != nil {return fmt.Errorf("panic occurred: %v", r)}}()return fn()})
}func main() {var g errgroup.GroupsafeGo(&g, func() error {// 可能触发panic的任务panic("unexpected error")return nil})if err := g.Wait(); err != nil {fmt.Println("Error:", err) // 输出 panic occurred: unexpected error}
}
3.3.2 封装增强版errgroup
import ("fmt""golang.org/x/sync/errgroup""sync"
)type SafeGroup struct {g errgroup.Groupmu sync.Mutexpanics []interface{}
}func (sg *SafeGroup) Go(fn func() error) {sg.g.Go(func() error {defer func() {if r := recover(); r != nil {sg.mu.Lock()sg.panics = append(sg.panics, r)sg.mu.Unlock()}}()return fn()})
}func (sg *SafeGroup) Wait() error {if err := sg.g.Wait(); err != nil {return err}if len(sg.panics) > 0 {return fmt.Errorf("panics occurred: %v", sg.panics)}return nil
}func main() {var sg SafeGroupsg.Go(func() error {panic("panic in goroutine")return nil})if err := sg.Wait(); err != nil {fmt.Println("Error:", err) // 输出: panics occurred: [panic in goroutine]}
}
这两种方案都能有效地在errgroup.Group中处理panic,方案一通过简单的函数封装,在每个任务中添加panic捕获;方案二则通过自定义结构体,将panic信息集中管理,在Wait方法中统一返回错误,方便在复杂场景下对panic进行更灵活的处理。
相关文章:

Go中的协程并发和并发panic处理
1 协程基础 1.1 协程定义(Goroutine) 概念:Go 语言特有的轻量级线程,由 Go 运行时(runtime)管理,相比系统线程(Thread),创建和销毁成本极低,占用…...

Qt Creator工具编译器配置
1、打开Qt Creator,工具-->选项 2、选择"编译器",Manual配置编译器。 初始化填入“C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin\amd64\cl.exe”,选择64位amd64。 ABI根据msvc版本进行选择msvc2015. 3、新建项目…...
从零开始的数据结构教程(六) 贪心算法
🍬 标题一:贪心核心思想——发糖果时的最优分配策略 贪心算法 (Greedy Algorithm) 是一种简单直观的算法策略。它在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望得到一个全局最优解。这就像你…...

Spring框架学习day7--SpringWeb学习(概念与搭建配置)
SpringWeb1.SpringWeb特点2.SpringWeb运行流程3.SpringWeb组件4.搭建项目结构图:4.1导入jar包4.2在Web.xml配置**4.2.1配置统一拦截分发器 DispatcherServlet**4.2.2开启SpringWeb注解(spring.xml) 5.处理类的搭建6.SpringWeb请求流程(自己理…...

打造高效多模态RAG系统:原理与评测方法详解
引言 随着信息检索与生成式AI的深度融合,检索增强生成(RAG, Retrieval-Augmented Generation) 已成为AI领域的重要技术方向。传统RAG系统主要依赖文本数据,但真实世界中的信息往往包含图像、表格等多模态内容。多模态RAG…...
SSM 框架核心知识详解(Spring + SpringMVC + MyBatis)
🌱 第一部分:Spring 核心原理与使用 1. 什么是 Spring Spring 是一个开源的 Java 企业级开发框架,旨在简化 Java 企业应用程序开发。它核心思想是控制反转(IoC)和面向切面编程(AOP)࿰…...
1.2 fetch详解
浏览器 Fetch API 详解 Fetch API 是现代浏览器提供的用于发起网络请求的接口,它基于 Promise 实现,替代了传统的 XMLHttpRequest,提供了更强大、更灵活的功能。 1. 基本用法 使用 fetch() 函数发起请求,返回一个 Promise&…...

【C#】Quartz.NET怎么动态调用方法,并且根据指定时间周期执行,动态配置类何方法以及Cron表达式,有请DeepSeek
🌹欢迎来到《小5讲堂》🌹 🌹这是《C#》系列文章,每篇文章将以博主理解的角度展开讲解。🌹 🌹温馨提示:博主能力有限,理解水平有限,若有不对之处望指正!&#…...

02 Deep learning神经网络的编程基础 逻辑回归--吴恩达
逻辑回归 逻辑回归是一种用于解决二分类任务(如预测是否是猫咪等)的统计学习方法。尽管名称中包含“回归”,但其本质是通过线性回归的变体输出概率值,并使用Sigmoid函数将线性结果映射到[0,1]区间。 以猫咪预测为例 假设单个样…...
Android Native 内存泄漏检测全解析:从原理到工具的深度实践
引言 Android应用的内存泄漏不仅发生在Java/Kotlin层,Native(C/C)层的泄漏同样普遍且隐蔽。由于Native内存不受Java虚拟机(JVM)管理,泄漏的内存无法通过GC自动回收,长期积累会导致应用内存占用…...
React---扩展补充
一些额外的扩展 4.3 高阶组件 高阶组件是参数为组件,返回值为新组件的函数; 高阶组件 本身不是一个组件,而是一个函数;其次,这个函数的参数是一个组件,返回值也是一个组件; import React fr…...
HTML 中 class 属性介绍、用法
1、🔖 什么是 class class 是 HTML 元素的一个核心属性,用来为元素指定一个或多个类名。它在网页开发中承担三大作用: 🎨 连接样式(CSS):让元素应用预定义的视觉效果⚙️ 绑定行为(…...

MySQL的并发事务问题及事务隔离级别
一、并发事务问题 1). 赃读:一个事务读到另外一个事务还没有提交的数据。 比如 B 读取到了 A 未提交的数据。 2). 不可重复读:一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读。 事务 A 两次读取同一条记录&…...

ProfiNet 分布式 IO 在某污水处理厂的应用
随着城市化进程的加速,污水处理厂的规模和复杂性不断增加,对自动化控制系统的要求也越来越高。PROfinet 分布式 IO 作为一种先进的工业通信技术,以其高速、可靠、灵活的特性,为污水处理厂的自动化升级提供了有力支持。本文将结合某…...
vue2使用笔记、vue2和vue3的区别
文章目录 vue2和vue3的区别1. 实现数据响应式的原理不同2. 生命周期不同3. vue 2.0 采用了 option 选项式 API,vue 3.0 采用了 composition 组合式 API4. 新特性编译宏5. 父子组件间双向数据绑定 v-model 不同6. v-for 和 v-if 优先级不同7. 使用的 diff 算法不同8.…...
Vue2数组数字字段求和技巧 数字求和方法
<template><div><p>总和: {{ totalSum }}</p></div> </template><script> export default {data() {return {items: [{ id: 1, value: 10 },{ id: 2, value: 20 },{ id: 3, value: 30 }]};},computed: {totalSum() {return this.ite…...

vue2 , el-select 多选树结构,可重名
人家antd都支持,elementplus 也支持,vue2的没有,很烦。 网上其实可以搜到各种的,不过大部分不支持重名,在删除的时候可能会删错,比如树结构1F的1楼啊,2F的1楼啊这种同时勾选的情况。。 可以全…...

Excel处理控件Aspose.Cells教程:使用 C# 从 Excel 进行邮件合并
邮件合并功能让您能够轻松批量创建个性化文档,例如信函、电子邮件、发票或证书。您可以从模板入手,并使用电子表格中的数据进行填充。Excel 文件中的每一行都会生成一个新文档,并在正确的位置包含正确的详细信息。这是一种自动化重复性任务&a…...
Jenkins | Jenkins构建成功服务进程关闭问题
Jenkins构建成功服务进程关闭问题 1. 原因2. 解决 1. 原因 Jenkins 默认会在构建结束时终止所有由构建任务启动的子进程,即使使用了nohup或后台运行符号&。 2. 解决 在启动脚本中加上 BULID_IDdontkillme #--------------解决jenkins 自动关闭进程问题-----…...
模块化架构下的前端调试体系建设:WebDebugX 与多工具协同的工程实践
随着前端工程化的发展,越来越多的项目采用模块化架构:单页面应用(SPA)、微前端、组件化框架等。这类架构带来了良好的可维护性和复用性,但也带来了新的调试挑战。 本文结合我们在多个模块化项目中的真实经验ÿ…...

EXCEL通过DAX Studio获取端口号连接PowerBI
EXCEL通过DAX Studio获取端口号连接PowerBI 昨天我分享了EXCEL链接模板是通过获取端口号和数据库来连接PowerBI模型的,链接:浅析EXCEL自动连接PowerBI的模板,而DAX Studio可以获取处于打开状态的PowerBI的端口号。 以一个案例分享如何EXCEL…...
PostgreSQL 技术峰会,为您打造深度交流优质平台
峰会背景 PostgreSQL 作为全球领先的开源关系型数据库管理系统,凭借其强大的功能、高度的扩展性和稳定性,在云计算、大数据、人工智能等领域得到了广泛应用。随着数字化转型的加速,企业对数据库技术的需求日益复杂和多样化,Postg…...
使用 OpenCV (C++) 进行人脸边缘提取
使用 OpenCV (C) 进行人脸边缘提取 本文将介绍如何使用 C 和 OpenCV 库来检测图像中的人脸,并提取这些区域的边缘。我们将首先使用 Haar级联分类器进行人脸检测,然后在检测到的人脸区域(ROI - Region of Interest)内应用 Canny 边…...

C# 委托UI控件更新例子,何时需要使用委托
1. 例子1 private void UdpRxCallBackFunc(UdpDataStruct info) {// 1. 前置检查防止无效调用if (textBoxOutput2.IsDisposed || !textBoxOutput2.IsHandleCreated)return;// 2. 使用正确的委托类型Invoke(new Action(() >{// 3. 双重检查确保安全if (textBoxOutput2.IsDis…...

大模型数据流处理实战:Vue+NDJSON的Markdown安全渲染架构
在Vue中使用HTTP流接收大模型NDJSON数据并安全渲染 在构建现代Web应用时,处理大模型返回的流式数据并安全地渲染到页面是一个常见需求。本文将介绍如何在Vue应用中通过普通HTTP流接收NDJSON格式的大模型响应,使用marked、highlight.js和DOMPurify等库进…...
python项目如何创建docker环境
这里写自定义目录标题 python项目创建docker环境docker配置国内镜像源构建一个Docker 镜像验证镜像合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPant…...
Eureka 高可用集群搭建实战:服务注册与发现的底层原理与避坑指南
引言:为什么 Eureka 依然是存量系统的核心? 尽管 Nacos 等新注册中心崛起,但金融、电力等保守行业仍有大量系统运行在 Eureka 上。理解其高可用设计与自我保护机制,是保障分布式系统稳定的必修课。本文将手把手带你搭建生产级 Eur…...

PyTorch--池化层(4)
池化层(Pooling Layer) 用于降低特征图的空间维度,减少计算量和参数数量,同时保留最重要的特征信息。 池化作用:比如1080p视频——720p 池化层的步长默认是卷积核的大小 ceil 允许有出界部分;floor 不允许…...
GPU加速与非加速的深度学习张量计算对比Demo,使用PyTorch展示关键差异
import torch import time # 创建大型随机张量 (10000x10000) tensor_size 10000 x_cpu torch.randn(tensor_size, tensor_size) x_gpu x_cpu.cuda() # 转移到GPU # CPU矩阵乘法 start time.time() result_cpu torch.mm(x_cpu, x_cpu.t()) cpu_time time.time() - sta…...
Vue中的自定义事件
一、前言 在 Vue 的组件化开发中,组件之间的数据通信是构建复杂应用的关键。而其中最常见、最推荐的方式之一就是通过 自定义事件(Custom Events) 来实现父子组件之间的交互。 本文将带你深入了解: Vue 中事件的基本概念如何在…...