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关键字即可。

go 复制代码
package main

import (
    "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,并观察其运行情况:

go 复制代码
package main

import (
    "fmt"
    "runtime"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    numGoroutines := 100000

    wg.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的内存消耗。

go 复制代码
package main

import (
    "fmt"
    "runtime"
    "sync"
)

func getGoroutineMemConsume() {
    var c chan int
    var wg sync.WaitGroup
    const goroutineNum = 1000000

    memConsumed := func() uint64 {
        runtime.GC() // GC,排除对象影响
        var memStat runtime.MemStats
        runtime.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在运行某个任务。

go 复制代码
type SingerConcurrencyRunner struct {
    isRunning bool
    sync.Mutex
}

func NewSingerConcurrencyRunner() *SingerConcurrencyRunner {
    return &SingerConcurrencyRunner{}
}

func (c *SingerConcurrencyRunner) markRunning() (ok bool) {
    c.Lock()
    defer c.Unlock()
    if c.isRunning {
        return false
    }
    c.isRunning = true
    return 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的数量来控制并发。例如,使用带缓冲的通道来控制并发数。

go 复制代码
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.ch
            if 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的并发能力,并在实际项目中有效利用这一强大特性。

相关推荐
Amagi.1 小时前
Spring中Bean的作用域
java·后端·spring
Dylanioucn1 小时前
【分布式微服务云原生】掌握 Redis Cluster架构解析、动态扩展原理以及哈希槽分片算法
算法·云原生·架构
2402_857589361 小时前
Spring Boot新闻推荐系统设计与实现
java·spring boot·后端
J老熊2 小时前
Spring Cloud Netflix Eureka 注册中心讲解和案例示范
java·后端·spring·spring cloud·面试·eureka·系统架构
Benaso2 小时前
Rust 快速入门(一)
开发语言·后端·rust
sco52822 小时前
SpringBoot 集成 Ehcache 实现本地缓存
java·spring boot·后端
原机小子2 小时前
在线教育的未来:SpringBoot技术实现
java·spring boot·后端
吾日三省吾码2 小时前
详解JVM类加载机制
后端
努力的布布2 小时前
SpringMVC源码-AbstractHandlerMethodMapping处理器映射器将@Controller修饰类方法存储到处理器映射器
java·后端·spring
PacosonSWJTU3 小时前
spring揭秘25-springmvc03-其他组件(文件上传+拦截器+处理器适配器+异常统一处理)
java·后端·springmvc