Go中的协程并发和并发panic处理

news2025/6/7 4:28:56

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,会导致以下问题:

  1. P 无法工作:P 的本地队列中可能有大量就绪的 G,但由于 M 被阻塞,这些 G 无法执行。
  2. CPU 核心浪费:如果 P 对应一个 CPU 核心,该核心将处于闲置状态,即使还有其他任务可执行。
  3. 因此,当 G 执行系统调用时,调度器会 解绑 M 和 P,允许 P 继续工作,避免 CPU 资源浪费
  • 系统调用返回后,G 重新加入某个 P 的队列等待执行。

2 并发模式 

2.1 共享内存并发

多个协程通过共享变量访问数据,需使用同步原语(如sync.Mutexsync.RWMutex)保护临界区

var (
    counter int
    mu      sync.Mutex
)

func increment() {
    mu.Lock()
    defer mu.Unlock()
    counter++
}

func main() {
    var wg sync.WaitGroup
    for 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 代码的执行顺序如下:

  1. 启动 goroutine 1:主协程创建并启动第一个匿名函数(// your code1),该函数在后台异步执行。

  2. 启动 goroutine 2:主协程紧接着创建并启动第二个匿名函数(// your code2),同样在后台异步执行。

  3. 主协程继续执行:主协程不会等待这两个 goroutine 完成,而是立即继续执行// go on之后的代码。

  4. 并行执行 goroutine// your code1// your code2的执行顺序取决于调度器,可能并行或交替执行,但它们的完成顺序不确定。由于主协程未等待它们,若主协程提前结束(例如程序退出),这两个 goroutine 可能被强制终止

2.3.1 sync.WaitGroup

wg.Wait()会阻塞直到2个协程执行完后

go func() { 
// func1
  wg.Done()
}()
go func() {
// func2
 wg.Done()
}()
wg.Wait()
// go on

这段 Go 代码的执行顺序如下:

  1. 启动 goroutine 1:主协程创建并启动第一个匿名函数(func1),该函数在后台异步执行。

  2. 启动 goroutine 2:主协程紧接着创建并启动第二个匿名函数(func2),同样在后台异步执行。

  3. 主协程阻塞:主协程执行wg.Wait(),进入阻塞状态,等待所有被等待的 goroutine 完成。

  4. 并行执行 goroutinefunc1func2的执行顺序取决于调度器,可能并行或交替执行,但它们的完成顺序不确定。每个 goroutine 在完成任务后调用wg.Done()通知等待组。

  5. 恢复主协程:当所有被等待的 goroutine(即func1func2)都调用了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失败)
}

执行顺序

  1. 主协程启动两个 goroutine 并行执行

  2. 若其中一个 goroutine 返回非 nil 错误:

    • 自动调用内置的context.CancelFunc

    • 向其他 goroutine 发送取消信号(通过 context)

    • g.Wait()立即返回首个错误

  3. 所有 goroutine(包括未出错的)需主动检查 context 状态并提前退出

2.3.3 对比

特性errgroup.Groupsync.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]int
    data["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  error
    Data 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.WaitGroup
    resultChan := make(chan Result)
    numWorkers := 3

    for 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.Group
    safeGo(&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.Group
    mu      sync.Mutex
    panics  []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 SafeGroup
    sg.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进行更灵活的处理。​

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2402459.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Qt Creator工具编译器配置

1、打开Qt Creator&#xff0c;工具-->选项 2、选择"编译器"&#xff0c;Manual配置编译器。 初始化填入“C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin\amd64\cl.exe”&#xff0c;选择64位amd64。 ABI根据msvc版本进行选择msvc2015. 3、新建项目…

Spring框架学习day7--SpringWeb学习(概念与搭建配置)

SpringWeb1.SpringWeb特点2.SpringWeb运行流程3.SpringWeb组件4.搭建项目结构图&#xff1a;4.1导入jar包4.2在Web.xml配置**4.2.1配置统一拦截分发器 DispatcherServlet**4.2.2开启SpringWeb注解&#xff08;spring.xml&#xff09; 5.处理类的搭建6.SpringWeb请求流程(自己理…

打造高效多模态RAG系统:原理与评测方法详解

引言 随着信息检索与生成式AI的深度融合&#xff0c;检索增强生成&#xff08;RAG, Retrieval-Augmented Generation&#xff09; 已成为AI领域的重要技术方向。传统RAG系统主要依赖文本数据&#xff0c;但真实世界中的信息往往包含图像、表格等多模态内容。多模态RAG&#xf…

【C#】Quartz.NET怎么动态调用方法,并且根据指定时间周期执行,动态配置类何方法以及Cron表达式,有请DeepSeek

&#x1f339;欢迎来到《小5讲堂》&#x1f339; &#x1f339;这是《C#》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解。&#x1f339; &#x1f339;温馨提示&#xff1a;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01;&#…

02 Deep learning神经网络的编程基础 逻辑回归--吴恩达

逻辑回归 逻辑回归是一种用于解决二分类任务&#xff08;如预测是否是猫咪等&#xff09;的统计学习方法。尽管名称中包含“回归”&#xff0c;但其本质是通过线性回归的变体输出概率值&#xff0c;并使用Sigmoid函数将线性结果映射到[0,1]区间。 以猫咪预测为例 假设单个样…

MySQL的并发事务问题及事务隔离级别

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

ProfiNet 分布式 IO 在某污水处理厂的应用

随着城市化进程的加速&#xff0c;污水处理厂的规模和复杂性不断增加&#xff0c;对自动化控制系统的要求也越来越高。PROfinet 分布式 IO 作为一种先进的工业通信技术&#xff0c;以其高速、可靠、灵活的特性&#xff0c;为污水处理厂的自动化升级提供了有力支持。本文将结合某…

vue2 , el-select 多选树结构,可重名

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

Excel处理控件Aspose.Cells教程:使用 C# 从 Excel 进行邮件合并

邮件合并功能让您能够轻松批量创建个性化文档&#xff0c;例如信函、电子邮件、发票或证书。您可以从模板入手&#xff0c;并使用电子表格中的数据进行填充。Excel 文件中的每一行都会生成一个新文档&#xff0c;并在正确的位置包含正确的详细信息。这是一种自动化重复性任务&a…

EXCEL通过DAX Studio获取端口号连接PowerBI

EXCEL通过DAX Studio获取端口号连接PowerBI 昨天我分享了EXCEL链接模板是通过获取端口号和数据库来连接PowerBI模型的&#xff0c;链接&#xff1a;浅析EXCEL自动连接PowerBI的模板&#xff0c;而DAX Studio可以获取处于打开状态的PowerBI的端口号。 以一个案例分享如何EXCEL…

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应用时&#xff0c;处理大模型返回的流式数据并安全地渲染到页面是一个常见需求。本文将介绍如何在Vue应用中通过普通HTTP流接收NDJSON格式的大模型响应&#xff0c;使用marked、highlight.js和DOMPurify等库进…

python项目如何创建docker环境

这里写自定义目录标题 python项目创建docker环境docker配置国内镜像源构建一个Docker 镜像验证镜像合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPant…

PyTorch--池化层(4)

池化层&#xff08;Pooling Layer&#xff09; 用于降低特征图的空间维度&#xff0c;减少计算量和参数数量&#xff0c;同时保留最重要的特征信息。 池化作用&#xff1a;比如1080p视频——720p 池化层的步长默认是卷积核的大小 ceil 允许有出界部分&#xff1b;floor 不允许…

2025年大模型平台落地实践研究报告|附75页PDF文件下载

本报告旨在为各行业企业在建设落地大模型平台的过程中&#xff0c;提供有效的参考和指引&#xff0c;助力大模型更高效更有价值地规模化落地。本报告系统性梳理了大模型平台的发展背景、历程和现状&#xff0c;结合大模型平台的特点提出了具体的落地策略与路径&#xff0c;同时…

PPTAGENT:让PPT生成更智能

想要掌握如何将大模型的力量发挥到极致吗&#xff1f;叶梓老师带您深入了解 Llama Factory —— 一款革命性的大模型微调工具。 1小时实战课程&#xff0c;您将学习到如何轻松上手并有效利用 Llama Factory 来微调您的模型&#xff0c;以发挥其最大潜力。 CSDN教学平台录播地址…

《汇编语言》第13章 int指令

中断信息可以来自 CPU 的内部和外部&#xff0c;当 CPU 的内部有需要处理的事情发生的时候&#xff0c;将产生需要马上处理的中断信息&#xff0c;引发中断过程。在第12章中&#xff0c;我们讲解了中断过程和两种内中断的处理。 这一章中&#xff0c;我们讲解另一种重要的内中断…

Redis实战-基于redis和lua脚本实现分布式锁以及Redission源码解析【万字长文】

前言&#xff1a; 在上篇博客中&#xff0c;我们探讨了单机模式下如何通过悲观锁&#xff08;synchronized&#xff09;实现"一人一单"功能。然而&#xff0c;在分布式系统或集群环境下&#xff0c;单纯依赖JVM级别的锁机制会出现线程并发安全问题&#xff0c;因为这…

计算机网络 : 应用层自定义协议与序列化

计算机网络 &#xff1a; 应用层自定义协议与序列化 目录 计算机网络 &#xff1a; 应用层自定义协议与序列化引言1. 应用层协议1.1 再谈协议1.2 网络版计算器1.3 序列化与反序列化 2. 重新理解全双工3. socket和协议的封装4. 关于流失数据的处理5. Jsoncpp5.1 特性5.2 安装5.3…

Python Day42 学习(日志Day9复习)

补充&#xff1a;关于“箱线图”的阅读 以下图为例 浙大疏锦行 箱线图的基本组成 箱体&#xff08;Box&#xff09;&#xff1a;中间的矩形&#xff0c;表示数据的中间50%&#xff08;从下四分位数Q1到上四分位数Q3&#xff09;。中位线&#xff08;Median&#xff09;&#…