Go Goroutine 究竟可以开多少?(详细介绍)

Go Goroutine 究竟可以开多少?

Go语言因其高效的并发处理能力而备受欢迎,而Goroutine则是Go语言实现并发编程的核心。Goroutine比传统的线程更加轻量,允许开发者轻松地处理大量并发任务。那么,Go语言中的Goroutine究竟可以开多少呢?在回答这个问题之前,我们需要先了解两个关键问题:

  1. Goroutine是什么?
  2. 开 Goroutine 需要消耗什么资源?因为最终的资源上限就决定了程序可以开多少Goroutine。

一、什么是Goroutine?

Goroutine是Go语言中的一种轻量级线程。与操作系统线程不同,Goroutine的创建和销毁成本极低。它们由Go运行时(runtime)管理,使用协作式调度(cooperative scheduling)来实现高效的并发处理。Goroutine的启动非常简单,只需要在函数调用前加上go关键字即可。

package mainimport ("fmt""time"
)func sayHello() {fmt.Println("Hello, Goroutine!")
}func main() {go sayHello()time.Sleep(time.Second)
}

在上述示例中,sayHello函数被作为一个Goroutine启动。

Goroutine的优势

  1. 轻量级:Goroutine比传统的操作系统线程要轻量得多。每个Goroutine大约只占用几KB的栈空间,并且栈是动态增长的。
  2. 简单易用:Goroutine的创建非常简单,只需要一个go关键字。
  3. 高效的调度:Go运行时通过M:N调度模型管理Goroutine,将M个Goroutine映射到N个操作系统线程上。这使得Goroutine可以在多核处理器上高效运行。

Goroutine的数量限制

理论上限

在理论上,Goroutine的数量几乎是无限的。由于每个Goroutine只占用少量内存,现代计算机的内存资源足以支持数百万甚至更多的Goroutine。然而,实际能开的Goroutine数量还受到其他因素的影响:

  1. 内存限制:每个Goroutine需要分配栈空间,虽然初始栈空间很小,但随着函数调用深度增加,栈空间会动态增长。内存限制是影响Goroutine数量的主要因素之一。
  2. CPU调度开销:虽然Goroutine的调度开销比操作系统线程低,但在极大量的Goroutine并发运行时,调度开销仍然不可忽视。
  3. 系统资源限制:操作系统和Go运行时对资源的管理也会影响Goroutine的实际数量。

实践中的经验

在实践中,Goroutine的数量通常不需要达到极限。以下是一些常见的经验和最佳实践:

  1. 合理设计并发:根据实际需求设计并发任务的数量,避免盲目创建大量Goroutine。
  2. 使用sync.WaitGroup管理并发:通过sync.WaitGroup可以方便地等待一组Goroutine完成。
  3. 监控和调优:使用Go语言提供的性能分析工具(如pprof)监控Goroutine的运行状况,及时发现和解决性能瓶颈。

代码示例:创建大量Goroutine

下面的示例代码演示了如何创建大量Goroutine,并观察其运行情况:

package mainimport ("fmt""runtime""sync"
)func main() {var wg sync.WaitGroupnumGoroutines := 100000wg.Add(numGoroutines)for i := 0; i < numGoroutines; i++ {go func(i int) {defer wg.Done()fmt.Printf("Goroutine %d\n", i)}(i)}wg.Wait()fmt.Println("All goroutines finished")// 打印当前运行的Goroutine数量fmt.Printf("Number of goroutines: %d\n", runtime.NumGoroutine())
}

在这段代码中,我们创建了10万个Goroutine,并使用sync.WaitGroup等待所有Goroutine完成。运行这段代码时,可以观察到系统资源的使用情况。

二. 开Goroutine需要消耗什么资源?

1. 内存的消耗

每个Goroutine需要消耗一定的内存,因为它们需要存储相应的数据结构。我们可以通过实验来测量开启Goroutine的内存消耗。

package mainimport ("fmt""runtime""sync"
)func getGoroutineMemConsume() {var c chan intvar wg sync.WaitGroupconst goroutineNum = 1000000memConsumed := func() uint64 {runtime.GC() // GC,排除对象影响var memStat runtime.MemStatsruntime.ReadMemStats(&memStat)return memStat.Sys}noop := func() {wg.Done()<-c // 防止Goroutine退出,内存被释放}wg.Add(goroutineNum)before := memConsumed() // 获取创建Goroutine前的内存for i := 0; i < goroutineNum; i++ {go noop()}wg.Wait()after := memConsumed() // 获取创建Goroutine后的内存fmt.Println(runtime.NumGoroutine())fmt.Printf("%.3f KB bytes\n", float64(after-before)/goroutineNum/1024)
}func main() {getGoroutineMemConsume()
}

结果分析:每个Goroutine大约消耗2KB的空间。假设计算机的内存是2GB,那么理论上最多可以开启2GB / 2KB ≈ 1百万个Goroutine。

2. CPU的消耗

Goroutine的调度和任务执行都会占用CPU资源。具体消耗的CPU资源取决于Goroutine执行的任务。如果任务是CPU密集型的计算,消耗的CPU资源会更多。

衡量一段代码能开多少协程同时并发运行,还需要考虑程序内的任务类型。如果是非常消耗内存的网络操作,可能几个Goroutine就能跑崩溃。如果是CPU密集型任务,可能几百个Goroutine就会让程序达到瓶颈。

三. Goroutine的实际应用

1. 如何控制并发数?

使用runtime.NumGoroutine()可以监控当前Goroutine的数量。为了避免过多的Goroutine导致资源耗尽,我们可以通过一些方法来控制并发数。

方法一:保证任务只有一个Goroutine在运行

可以通过设置一个运行标志(running flag)来确保同一时间只有一个Goroutine在运行某个任务。

type SingerConcurrencyRunner struct {isRunning boolsync.Mutex
}func NewSingerConcurrencyRunner() *SingerConcurrencyRunner {return &SingerConcurrencyRunner{}
}func (c *SingerConcurrencyRunner) markRunning() (ok bool) {c.Lock()defer c.Unlock()if c.isRunning {return false}c.isRunning = truereturn true
}func (c *SingerConcurrencyRunner) unmarkRunning() {c.Lock()defer c.Unlock()c.isRunning = false
}func (c *SingerConcurrencyRunner) Run(f func()) {if !c.markRunning() {return}go func() {defer func() {if err := recover(); err != nil {// log error}c.unmarkRunning()}()f()}()
}
方法二:任务有指定的协程数运行

通过限制Goroutine的数量来控制并发。例如,使用带缓冲的通道来控制并发数。

type ProcessFunc func(ctx context.Context, param interface{})type MultiConcurrency struct {ch chan struct{}f  ProcessFunc
}func NewMultiConcurrency(size int, f ProcessFunc) *MultiConcurrency {return &MultiConcurrency{ch: make(chan struct{}, size),f:  f,}
}func (m *MultiConcurrency) Run(ctx context.Context, param interface{}) {m.ch <- struct{}{}go func() {defer func() {<-m.chif err := recover(); err != nil {fmt.Println(err)}}()m.f(ctx, param)}()
}func mockFunc(ctx context.Context, param interface{}) {fmt.Println(param)
}func main() {concurrency := NewMultiConcurrency(10, mockFunc)for i := 0; i < 1000; i++ {concurrency.Run(context.Background(), i)if runtime.NumGoroutine() > 13 {fmt.Println("goroutine", runtime.NumGoroutine())}}
}

通过这种方式,可以有效控制同时运行的Goroutine数量,防止内存和CPU资源被耗尽。

四. 常见的问题

1. Too many open files

如果程序中有大量打开的文件或socket没有及时关闭,可能会遇到"too many open files"的错误。这时需要检查任务中是否有大量打开的文件或socket连接,并确保它们在不需要时及时关闭。

2. Out of memory

如果Goroutine泄露,即不断创建Goroutine但没有结束,可能会导致内存耗尽。需要确保Goroutine能够正常退出,并在适当的时候进行垃圾回收。

结论

Goroutine是Go语言并发编程的强大工具,其轻量级和高效的特点使得Go程序可以轻松处理大量并发任务。虽然理论上Goroutine的数量几乎没有限制,但在实际应用中,我们仍需考虑内存、CPU调度开销和系统资源限制等因素。合理设计并发任务、监控和调优是确保系统稳定性和高效性的关键。

希望本文能帮助你更好地理解Goroutine的并发能力,并在实际项目中有效利用这一强大特性。

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

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

相关文章

6.1触发器的创建、删除和使用

6.1.1 触发器概述 触发器是一个被指定关联到表的数据库对象&#xff0c;与表的关系密切&#xff0c;不需要用户调用&#xff0c;在一个表的特定事件出现时将会被激活&#xff0c;此时某些MySql语句会自动执行。 触发器用于实现数据库的完整性&#xff0c;具有以下特点&#xff…

深入了解RSA加密算法

目录 前言 一、什么是RSA&#xff1f; 二、RSA加密的基本概念 1.非对称加密 2.密钥生成 3.加密和解密 三、RSA加密的工作原理 四、RSA的应用场景 五、RSA加密解密的实现 六、RSA算法的局限性及改进措施 前言 在当今的数字化时代&#xff0c;信息的安全性成为了人们关注…

密码学-密码协议之零知识证明

一、前言 零知识证明实际上一种密码协议&#xff0c;该协议的一方称为证明者(Prover)&#xff0c;通常用P表示&#xff0c;协议的另一方是验证者(Verifier)&#xff0c;一般用V表示。零知识证明是指P试图使V相信某个论断是正确的&#xff0c;但却不向V提供任何有用的信息&…

Photoshop中图像美化工具的应用

Photoshop中图像美化工具的应用 Photoshop中的裁剪工具Photoshop中的修饰工具模糊工具锐化工具涂抹工具 Photoshop中的颜色调整工具减淡工具加深工具海绵工具 Photoshop中的修复工具仿制图章工具污点修复画笔工具修复画笔工具修补工具内容感知移动工具红眼工具 Photoshop中的裁…

【杂记-浅谈以太网IP数据帧】

一、以太网数据帧 以太网数据帧是网络通信的基础单元&#xff0c;遵循IEEE 802.3标准&#xff0c;用于在以太网中传输数据。一个典型的以太网数据帧包括前导码、帧开始符、目的MAC地址、源MAC地址、类型或长度字段、数据载荷和帧校验序列。其中&#xff0c;类型字段指明了上层…

数据可视化:让数据讲述故事

数据可视化&#xff1a;让数据讲述故事 引言 在信息爆炸的时代&#xff0c;数据无处不在。然而&#xff0c;数据本身并不直观&#xff0c;很难从中直接获取有价值的信息。数据可视化作为一种强大的工具&#xff0c;能够帮助我们揭示数据背后的故事&#xff0c;让我们以更直观…

Spring Boot顶层接口实现类注入项目的方法

1、背景 在项目中&#xff0c;我们通常会具有同一特性的业务类定义一个顶层接口&#xff0c;让业务类实现这个接口&#xff0c;通过接口规范来管理这些类。我们将这些实现接口的业务类交托给Spring容器接口后&#xff0c;有时候需要根据业务类型来选择动态选择对应的业务类阿里…

尚品汇-(二)

一&#xff1a;环境安装 1.安装JAVA 运行环境 第一步&#xff1a;上传或下载安装包 cd /usr/local jdk-8u152-linux-x64.tar.gz 第二步&#xff1a;解压安装包 tar -zxvf jdk-8u152-linux-x64.tar.gz 第三步&#xff1a;建立软连接&#xff08;快捷方式&#xff09; ln -…

SpringBoot配置第三方专业缓存技术Redis

Redis缓存技术 Redis&#xff08;Remote Dictionary Server&#xff09;是一个开源的内存中数据结构存储系统&#xff0c;通常用作数据库、缓存和消息中间件。它支持多种数据结构&#xff0c;如字符串、哈希表、列表、集合、有序集合等&#xff0c;并提供了丰富的功能和灵活的…

4.1 初探Spring Boot

初探Spring Boot实战概述 Spring Boot简介 Spring Boot是一个开源的Java框架&#xff0c;由Pivotal团队&#xff08;现为VMware的一部分&#xff09;开发&#xff0c;旨在简化Spring应用程序的创建和部署过程。它通过提供一系列自动化配置、独立运行的特性和微服务支持&#…

类加载的初始化阶段的奥秘

一、概述 初始化阶段是类加载机制&#xff08;加载&#xff0c;链接&#xff08;验证&#xff0c;准备&#xff0c;解析&#xff09;&#xff0c;初始化&#xff09;的最后一步。在准备阶段已经为类变量赋过一次值&#xff08;默认为0或null&#xff09;。在初始化阶段&#xf…

HTML和CSS基础(一)

前言 HTML&#xff08;HyperText Markup Language&#xff09;是一种用于创建网页的标准标记语言。它由各种标签组成&#xff0c;这些标签定义了网页的结构和内容。HTML的早期形式诞生于1989年&#xff0c;由CERN的物理学家Tim Berners-Lee发明&#xff0c;最初用于在科学家之…

js 刷题常用方法

Object 对象共有 toString()valueOf()obj.keys()obj.values()obj.entries()for of 循环…扩展运算符Array.of()Array.from() 数组使用时 toString 返回以逗号分隔的字符串valueOf 返回数组本身 Array 数组 创建数组 Array.from(可迭代对象)Array.of(传入一组参数) let arr…

C语言 | Leetcode C语言题解之第160题相交链表

题目&#xff1a; 题解&#xff1a; struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {if (headA NULL || headB NULL) {return NULL;}struct ListNode *pA headA, *pB headB;while (pA ! pB) {pA pA NULL ? headB : pA->ne…

windows11 x64 23H2 企业纯净版2024.6.16

闲来无事试安装了下da_nao_yan的 【6月12日更新】Windows11 22631.3737企业版 23H2 自用优化版 &#xff08;原版地址&#xff1a;https://bbs.pcbeta.com/viewthread-1985546-1-1.html&#xff09;&#xff0c;感觉比原版流畅多了&#xff0c;重新按照自己习惯封装了下&#x…

C++作业第四天

#include <iostream> using namespace std; class Per { private: string name; int age; int *high; double *weight; public: //构造函数 Per() { cout << "Per的无参构造" << endl; } Per(str…

Python获取一个列表的全组合

numList [1, 3, 5, 7, 9] from itertools import combinationsfor i in range(1, len(numList) 1): # xrange will return the values 1,2,3,4 in this loopprint(list(combinations(numList, i)))输出&#xff1a; [(1,), (3,), (5,), (7,), (9,)] [(1, 3), (1, 5), (1, 7)…

通讯的一些基本概念 -网卡 -网段 -网桥 -路由表

网卡&#xff08;Network Interface Card, NIC&#xff09; 网卡是安装在计算机或其他设备上的硬件组件&#xff0c;它允许设备通过有线或无线方式连接到计算机网络。网卡工作在OSI模型的数据链路层&#xff08;第二层&#xff09;和物理层&#xff08;第一层&#xff09;&…

排序——希尔排序

希尔排序实际上是插入排序的优化&#xff0c;所以要先介绍插入排序。 目录 插入排序 思想 演示 代码实现 总结 希尔排序 思想 演示 代码 总结 插入排序 思想 又称直接插入排序。它的基本思想是将一个值插入到一个有序序列中。直至将所有的值都插入完。 演示 假设数…

Web前端开发的过程:深入剖析与精彩演绎

Web前端开发的过程&#xff1a;深入剖析与精彩演绎 在数字化时代&#xff0c;Web前端开发作为构建用户界面的关键环节&#xff0c;其重要性不言而喻。这一过程涉及众多技术细节和创意构思&#xff0c;充满了挑战与机遇。本文将从四个方面、五个方面、六个方面和七个方面&#…