• 控制Goroutine并发量的解决方案


    前言

      Go语言虽然开并发Goroutine特别简单,但是实际中如果不控制并发的数量会导致资源的浪费以及同时占用大量服务资源(http连接、数据库连接、文件句柄等)导致服务性能下降!

      笔者之前总结过一篇在业务代码中控制并发数量的文章:Go控制协裎并发数量的用法及实际中的一个案例

    ants库实现链接池的效果控制并发量

      今天介绍另外一个控制并发数量的第三方库:ants

      简而言之,ants库通过实现“Goroutine链接池”来限制Goroutine的数量:通过NewPool函数创建一个goroutine pool实现具体效果。

      创建完 goroutine pool 后,通过 pool.Submit 方法向 pool 中提交任务。
      如果 pool 中尚有空闲的 goroutine worker,则 pool.Submit 立即返回;否则根据 pool 的配置,pool.Submit 立即返回错误或等待有空闲 goroutine worker 成功接收任务后返回。

    使用案例

      使用之前记当然是 go get一下:

    go get github.com/panjf2000/ants

    基本使用

      最基本的使用场景是:提交任务,等待任务完成并获取结果。

    package test1
    
    import (
        "fmt"
        "github.com/panjf2000/ants/v2"
        "sync"
        "testing"
    )
    
    func sum(a, b int) int {
        return a + b
    }
    
    func wrapSum(i int, ch chan int, wg *sync.WaitGroup) func() {
        return func() {
            defer wg.Done()
            ch <- sum(i, i)
        }
    }
    
    func TestT1(t *testing.T) {
    
        var wg sync.WaitGroup
        ch := make(chan int, 10)
        // ants.Release 相当于调用 defaultPool.Release,停止 defaultPool 中所有的 goroutine worker.
        defer ants.Release()
        for i := 0; i < 10; i++ {
            wg.Add(1)
            // ants.Submit 相当于调用 defaultPool.Submit,而 defaultPool 是在 package 初始化时 ants 库创建的
            if err := ants.Submit(wrapSum(i, ch, &wg)); err != nil {
                return
            }
        }
        wg.Wait()
        close(ch)
        for v := range ch {
            fmt.Println(v)
        }
    
    }
    g1_test.go

      需要注意以下几点:

      1、这里使用的是ants包默认的链接池(ants.Submit方法),打开源码可以看到链接池的容量大小为: math.MaxInt32*(2147483647),所以实际中推荐大家自己控制链接池的容量大小。

      2、Submit 方法只接受 func() 类型的参数,如果提交的任务有参数,需要自己 wrap。

      3、ants 没有提供返回值机制,任务的执行结果需要自己进行处理,例子中用了一个带 buffer 的 channel。需要注意的是,当 pool 中有多个任务时,任务的返回值不是根据任务的提交顺序进行排序的,任务的返回顺序取决于调用时机,可以认为是随机的。

      4、ants 没有提供等待所有任务完成的机制,例子中用了 sync.WaitGroup 实现了等待所有任务完成的机制,否则 main goroutine 可能会在任务执行结束前退出。

    配置pool为nonblocking状态的情况

      以下示例将 pool 配置为 nonblocking。在这种情况下,当 pool 中没有 可用的 goroutine worker 时,Submit 会直接返回错误 ants.ErrPoolOverload,而不会等待提交任务成功才返回。

      另外这里可以配置链接池的大小(ants.NewPool方法):

    package test1
    
    import (
        "fmt"
        "github.com/panjf2000/ants/v2"
        "testing"
    )
    
    func hangForever() {
        ch := make(chan int)
        ch <- 10
    }
    
    func TestT2(t *testing.T) {
        pool, err := ants.NewPool(10, ants.WithNonblocking(true))
        if err != nil {
            return
        }
        defer pool.Release()
        for i := 0; i < 10; i++ {
            if err := pool.Submit(hangForever); err != nil {
                return
            }
        }
        if err := pool.Submit(func() { fmt.Println("hello") }); err != nil {
            fmt.Printf("err=ErrPoolOverload:%t
    ", err == ants.ErrPoolOverload)
        }
    }
    g2_test.go

    关于超时任务的处理 ***

      在实际中我们往往会希望在摸一个Goroutine执行任务超时或者其他一些情况下退出而不是一直占用着资源!

      但是由于线程才是操作系统可调度的最小的单位,Goroutine是代码级别的并发,由于GMP模型的限制,我们并不能确定开启的子Goroutine什么时候执行,Go中也没有像epoll那样的“轮询机制”——专门开一个协程去轮询其他的子Goroutine管理它们,所以想要真正的去实现子Goroutine的超时退出需要程序员们在业务代码中做相应的逻辑处理。
      我这里使用context去简单处理超时的Goroutine:

    package test1
    
    import (
        "context"
        "fmt"
        "github.com/panjf2000/ants/v2"
        "testing"
        "time"
    )
    
    func expensiveTask2(ctx context.Context, a, b int) (int, error) {
        select {
        // simulate an expensive task
        case <-time.After(10 * time.Second):
            return a + b, nil
        case <-ctx.Done():
            return 0, ctx.Err()
        }
    }
    
    func wrap2(ctx context.Context) func() {
        return func() {
            sum, err := expensiveTask2(ctx, 10, 20)
            if err != nil {
                fmt.Printf("error is %v
    ", err)
            } else {
                fmt.Printf("sum is %d
    ", sum)
            }
        }
    }
    
    func TestT31(t *testing.T) {
        pool, err := ants.NewPool(1)
        if err != nil {
            return
        }
        ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
        defer cancel()
        if err := pool.Submit(wrap2(ctx)); err != nil {
            return
        }
        // wait other goroutines.
        for i := 0; i < 20; i++ {
            time.Sleep(time.Second)
            fmt.Printf("main waits for %d seconds
    ", i+1)
        }
    }
    g3_test.go

      以上的例子中,在提交任务时,向任务传递了一个 3 秒钟超时的 context。
    在任务函数的逻辑中,通过 Done() 方法等待停止信号(超时或被 main goroutine 主动 cancel),从而使任务函数在一定的时机结束,避免一直执行下去。

      需要注意的是,expensiveTask 函数中用了 select 来等待 Done() 的返回,在业务逻辑的哪个时机等待 Done ,需要开程序员自己去设计!

  • 相关阅读:
    005. Asp.Net Routing与MVC 之三: 路由在MVC的使用
    004. Asp.Net Routing与MVC 之二: 请求如何激活Controller和Action
    001. Asp.Net Routing与MVC 之(基础知识):URL
    002. Asp.Net Routing与MVC 之(基础知识):HttpModule 与 HttpHandler
    003. Asp.Net Routing与MVC 之一: 请求如何到达MVC
    Factory
    decorator
    Java 单例真的写对了么?
    Dubbo Jackson序列化使用说明
    使用JavaConfig方式配置dubbox
  • 原文地址:https://www.cnblogs.com/paulwhw/p/14482347.html
Copyright © 2020-2023  润新知