Golang根据job数量动态控制每秒协程的最大创建数量

需求:第三方的接口,限制接口请求的QPS,每秒5次

需要控制job「访问接口」的次数,每秒不能同时超过5次包括 进行中的任务、刚启动的任务

  要确保单位时间内(例如每秒)运行的任务数量不超过特定的上限(如5个任务),并且在任务执行完成得很快时,考虑已完成的任务和正在执行的任务作为正在运行的任务总数,可以使用限流器来控制任务的启动频率,并结合使用信号量来管理同时运行的任务数量。

  具体来说,使用一个信号量来限制同时进行的任务数量,并且在任务完成时,仅在下一秒钟允许新的任务开始,以确保即使某些任务快速完成,也不会在同一秒钟内启动超过限制数量的任务。

go 复制代码
package main

import (
    "context"
    "fmt"
    "math/rand"
    "sync"
    "sync/atomic"
    "time"

    "golang.org/x/time/rate"
)

func RateLimit() {
    const maxJobsPerSecond = 5
    const numJobs = 22
    var wg sync.WaitGroup

    // 计数器
    var runningJobs int32 // 当前正在执行的任务数量
    var startedJobs int32 // 启动后的任务数量
    var finishedJobs int32 // 刚完成的任务数量

    limiter := rate.NewLimiter(rate.Every(time.Second/time.Duration(maxJobsPerSecond)), maxJobsPerSecond)
    semaphore := make(chan struct{}, maxJobsPerSecond)

    for i := 1; i <= numJobs; i++ {
        wg.Add(1)
        go func(jobID int) {
            defer wg.Done()
            limiter.Wait(context.Background()) // 等待限流器允许进行下一个任务

            semaphore <- struct{}{} // 获取信号量
            atomic.AddInt32(&startedJobs, 1)
            atomic.AddInt32(&runningJobs, 1)

            executeJob(jobID) // 执行任务
            atomic.AddInt32(&finishedJobs, 1)
            atomic.AddInt32(&runningJobs, -1)

            <-time.After(time.Second) // 等待一秒钟后释放信号量
            <-semaphore

            // 打印当前状态
            printStatus(&runningJobs, &startedJobs, &finishedJobs)
        }(i)
    }

    wg.Wait()
    fmt.Println("所有工作完成")
}

注意事项

  • 限流器 rate.NewLimiter 用于控制任务启动的频率,以确保每秒不超过 maxJobsPerSecond 个任务开始执行。
  • 使用信号量 semaphore 来控制同时进行的任务数量。
  • 为了确保在任何一秒内同时进行的任务数量不超过限制,在任务完成后等待一秒钟,然后再释放信号量。这样做可以保证即使任务很快完成,也不会立即启动新的任务。

  这种实现方式确保了即使任务执行得很快,每秒钟启动的新任务数量也不会超过限制,并且同时考虑了正在执行和刚刚完成的任务。

动态创建协程

  • 协程的启动是动态的。在代码中,每个任务对应于一个动态创建的协程。这些协程是在循环中根据任务数量(numJobs)动态生成的。

  • 具体来说,每当有一个新的任务需要执行时,都会创建一个新的协程来处理这个任务。这是通过在 main 函数的循环中调用 go 关键字实现的。这个过程在每次循环迭代中发生,从而为每个任务动态创建一个新的协程。

  • 由于使用了限流器(rate.Limiter),这些协程不是一次性全部创建,而是根据限流器允许的速率逐个创建。每个协程在开始执行任务之前会等待限流器的许可,以此确保每秒启动的任务数量不超过设定的最大值。

go 复制代码
func executeJob(jobID int) {
	startTime := time.Now() // 记录任务开始时间
	
    // 模拟任务执行时间
    fmt.Printf("%v Job %d started\n",time.Now().Format("2006-01-02 15:04:05.000"), jobID)
	// 初始化随机数种子
	rand.Seed(time.Now().UnixNano())
	// 随机生成一个时间间隔(例如,1到5000毫秒之间)
	min := 1
	max := 5000
	duration := time.Duration(rand.Intn(max-min+1)+min) * time.Millisecond
	time.Sleep(duration)

	durationCost := time.Since(startTime) // 计算任务耗时

    fmt.Printf("%v Job %d finished Cost:%v\n", time.Now().Format("2006-01-02 15:04:05.000"),jobID, durationCost)
}

func printStatus(runningJobs, startedJobs, finishedJobs *int32) {
    fmt.Printf("Current status - Running: %d, Started: %d, Finished: %d\n",
        atomic.LoadInt32(runningJobs),
        atomic.LoadInt32(startedJobs),
        atomic.LoadInt32(finishedJobs))
}

  可以在代码中添加额外的逻辑来跟踪和打印正在执行、进行中、刚启动和刚完成的任务数量。使用原子操作(来自 sync/atomic 包)来确保在并发环境下对这些计数器的操作是安全的。

在这个示例中:

  • 使用 sync/atomic 包中的 AddInt32LoadInt32 来安全地增加和读取计数器的值。
  • 在每个任务开始时,增加 startedJobsrunningJobs 计数器。
  • 在每个任务完成时,增加 finishedJobs 计数器,并减少 runningJobs 计数器。
  • 在任务完成后和释放信号量前,打印当前的任务状态。

注意事项

  • 这种方法可以帮助我们跟踪不同状态下的任务数量。
  • 使用原子操作确保在并发环境中对计数器的读写是安全的。
  • printStatus 函数在每个任务的结束时被调用,以打印当前的任务状态。
相关推荐
骄马之死3 小时前
SpringMVC + SpringBoot 核心知识点总结
java·spring boot·后端
GoGeekBaird4 小时前
Anthropic技能"(Skills)的经验分享
后端
王码码20355 小时前
多台服务器怎么统一看状态?Beszel 轻量监控,搭起来不费事
运维·服务器·后端·安全·阿里云·接口·web
刀法如飞5 小时前
一文搞懂DDD 领域驱动设计思想原理
设计模式·架构·代码规范
郑洁文5 小时前
基于Spring Boot的流浪动物救助网站
java·spring boot·后端·毕设·流浪动物救助
Cosolar6 小时前
LlamaIndex 文档解析与分块策略深度解析
人工智能·面试·架构
指令集梦境6 小时前
Cursor + Spring Boot实战:从零写一个RESTful API
spring boot·后端·restful
摇滚侠7 小时前
Maven 入门+高深 单一架构案例 54-59
java·架构·maven·intellij-idea
caimouse7 小时前
Reactos 第 4 章 对象管理 — 4.5 几个常用的内核函数
c语言·windows·架构
码云之上7 小时前
聊聊如何设计一个高效、稳定的 Node.js 接入层
前端·后端·node.js