使用golang写个简单的协程池

本文介绍了使用协程池处理大量数据的优势,如提高效率和限制并发数,以避免CPU利用率过高。通过Go语言实现了一个简单的协程池示例,展示了如何通过队列和工作协程来调度任务。测试结果显示,使用协程池显著提升了请求处理速度。然而,该方案也存在局限性,如对外部系统依赖的风险,需要考虑错误重试和数据一致性策略。
摘要由CSDN通过智能技术生成

为什么需要协程池?
1、我在处理几十万数据时候,因为无数次开启协程,报错协程开太多,cpu利用率飙升,无法再继续跑任务了

2、规定好,这个程序,只能开启多少个协程,例如3个协程去跑,就可以避免1的情况

3、非常高效和快速,根据我自己的业务逻辑,请求别的系统,存在修改,不存在新增,然后我这边要同步,3个协程去处理,1秒钟能处理20多个任务(当然这个也是要考虑别人系统的,别人系统是php写的)

 

大致架构

EntryChannel:入口方法,所有task任务都要扔到EntryChannel方法中,方便解耦,不可能直接扔队列JobsChannel中的,这样会导致代码太过拥挤、难管理,如果以后有修改,岂不是整个都得改?


JobsChannel:队列,为什么用队列呢?因为这个队列是先进先出的,排序功能有了,然后这个队列会一直去读取EntryChannel入口的task任务,利用golang的 for range 的特性,如果存在就获取,不存在就等待的特性

worker:处理任务的协程(goroutines),这里是负责处理业务逻辑的。

上代码:
 

文件

pool.go

package main

import (
	"fmt"
	"time"
)

// ------------------------------------ 有关task任务角色的功能 ---------------------------------
// 定义一个任务类型 task
type Task struct {
	f func() error // 一个Task里面应该有一个具体的业务,这个业务的逻辑就是f这个函数
	// 后续优化,可以在这里加一个任务优先级,然后在取的时候判断优先级
}

// 创建一个task任务
// arg_f 任务逻辑
func NewTask(arg_f func() error) *Task { // 返回一个任务
	t := Task{
		f: arg_f,
	}
	return &t
}

// task也需要一个执行业务的方法 -> 任务逻辑
func (t *Task) Execute() {
	t.f() // 调用任务中已经绑定好的业务方法
}

// ------------------------------------ 有关task任务角色的功能 ---------------------------------

// ------------------------------------ 有关协程池pool角色的功能 ---------------------------------

// 定义一个pool协程池的类型
type Pool struct {
	// 对外的task入口 EntryChannel -> 把task仍进入口
	EntryChannel chan *Task

	// 内部的task队列 JobsChannel
	JobsChannel chan *Task

	// 协程池中最大的worker的数量
	Worker_num int
}

// 创建 Pool 的函数
func NewPool(cap int) *Pool {
	// 创建一个Pool
	p := Pool{
		EntryChannel: make(chan *Task),
		JobsChannel:  make(chan *Task),
		Worker_num:   cap,
	}
	// 返回这个Pool
	return &p
}

// 协程池创建一个worker,并且让这个worker去工作
func (p *Pool) Worker(worker_id int) {
	// 一个worker具体的工作

	// 1 永久的从jobsChannel去取任务
	// for range 的语法就是 如果p.JobsChannel没有任务会停住,等到有数据又会继续执行
	for task := range p.JobsChannel {
		// task就是当前worker从 jobsChaneel中拿到的任务
		// 2 一旦取到任务就去执行这个任务 , 后续优化这里可以加上优先级排序再执行任务
		task.Execute()
		fmt.Println("worker Id", worker_id, "执行完了一个任务")
	}
}

// 让协程池开始真正的工作,这是一个协程池启动的方法
func (p *Pool) run() {
	// 1 根据worker_num来创建worker去工作
	for i := 0; i < p.Worker_num; i++ {
		// 每一个worker就是一个goroutine
		go p.Worker(i)
	}
	// 2 不断地从EntryChannel中取任务,将取道的任务发送给JobsChannel
	for task := range p.EntryChannel {
		// 一旦有task读到就要交给JobsChannel,交给他之后就会被上面的 Worker方法中的for range 取到
		p.JobsChannel <- task
	}
}

// 主函数
func main() {
	// 1 创建一些任务
	t := NewTask(func() error {
		// 当前任务的业务 -> 打印出当前的系统时间
		fmt.Println(time.Now())
		return nil
	})

	// 2 创建一个 Pool协程池, 这个协程池最大的worker数量是4
	p := NewPool(4)

	// 执行了的任务数量
	task_num := 0

	// 3 将这些任务交给协程池Pool
	go func() {
		for {
			// 不断向p中去写入任务t,每个任务都是打印当前时间
			p.EntryChannel <- t // 向入口一直写任务
			task_num += 1
			fmt.Println("当前一共执行了", task_num, "任务")
		}
	}()

	// 4 启动pool, 让Pool开始工作,此时pool会创建worker,让worker工作
	// 为了让 p.run 执行,上面就一定要开启协程,不然只会死循环,永远都不会执行到p.run
	p.run()
}

执行

go run pool.go

 

在公司业务中,我做了以下测试

没有用协程时候的请求时间

是不是非常慢?


当我用了协程并且设计了协程池的请求速度

怎么样?感受到它的威力了吗?

 

局限
    当然,我这个简单的协程池功能,也有局限的,就是当你去另一个系统中请求时候,如果另一个系统报错了,或者另一个系统无法支持我这么多个请求而导致崩了,或者数据丢失了,这时候就需要有一个重新请求的逻辑,大家有更好的意见吗?

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值