从零学习 Go 语言(33):如何手动实现一个协程池?

栏目: IT技术 · 发布时间: 4年前

内容简介:在线博客:http://golang.iswbm.com/Github:https://github.com/iswbm/GolangCodingTime

从零学习  <a href='https://www.codercto.com/topics/6127.html'>Go</a>  语言(33):如何手动实现一个协程池?

在线博客:http://golang.iswbm.com/

Github:https://github.com/iswbm/GolangCodingTime

在 Golang 中要创建一个协程是一件无比简单的事情,你只要定义一个函数,并使用 go 关键字去执行它就行了。

如果你接触过其他语言,会发现你在使用使用线程时,为了减少线程频繁创建销毁还来的开销,通常我们会使用线程池来复用线程。

池化技术就是利用复用来提升性能的,那在 Golang 中需要协程池吗?

在 Golang 中,goroutine 是一个轻量级的线程,他的创建、调度都是在用户态进行,并不需要进入内核,这意味着创建销毁协程带来的开销是非常小的。

因此,我认为大多数情况下,开发人员是不太需要使用协程池的。

但也不排除有某些场景下是需要这样做,因为我还没有遇到就不说了。

抛开 是否必要 这个问题,单纯从技术的角度来看,我们可以怎样实现一个通用的协程池呢?

下面就来一起学习一下我的写法

首先定义一个协程池(Pool)结构体,包含两个属性,都是 chan 类型的。

一个是 work,用于接收 task 任务

一个是 sem,用于设置协程池大小,即可同时执行的协程数量

type Pool struct {
    work chan func()   // 任务
    sem  chan struct{} // 数量
}

然后定义一个 New 函数,用于创建一个协程池对象,有一个细节需要注意

work 是一个无缓冲通道

而 sem 是一个缓冲通道,size 大小即为协程池大小

func New(size int) *Pool {
    return &Pool{
        work: make(chan func()),
        sem:  make(chan struct{}, size),
    }
}

最后给协程池对象绑定两个函数

1、 NewTask :往协程池中添加任务

当第一次调用 NewTask 添加任务的时候,由于 work 是无缓冲通道,所以会一定会走第二个 case 的分支:使用 go worker 开启一个协程。

func (p *Pool) NewTask(task func()) { 
    select {
        case p.work <- task:
        case p.sem <- struct{}{}:
            go p.worker(task)
    }
}

2、 worker :用于执行任务

为了能够实现协程的复用,这个使用了 for 无限循环,使这个协程在执行完任务后,也不退出,而是一直在接收新的任务。

func (p *Pool) worker(task func()) { 
    defer func() { <-p.sem }()
    for {
        task()
        task = <-p.work
    }
}

这两个函数是协程池实现的关键函数,里面的逻辑很值得推敲:

1、如果设定的协程池数大于 2,此时第二次传入往 NewTask 传入task,select case 的时候,如果第一个协程还在运行中,就一定会走第二个case,重新创建一个协程执行task

2、如果传入的任务数大于设定的协程池数,并且此时所有的任务都还在运行中,那此时再调用 NewTask 传入 task ,这两个 case 都不会命中,会一直阻塞直到有任务执行完成,worker 函数里的 work 通道才能接收到新的任务,继续执行。

以上便是协程池的实现过程。

使用它也很简单,看下面的代码你就明白了

func main()  {
    pool := New(128)
    pool.NewTask(func(){
        fmt.Println("run task")
    })
}

为了让你看到效果,我设置协程池数为 2,开启四个任务,都是 sleep 2 秒后,打印当前时间。

func main()  {
    pool := New(2)

    for i := 1; i <5; i++{
        pool.NewTask(func(){
            time.Sleep(2 * time.Second)
            fmt.Println(time.Now())
        })
    }
    
    // 保证所有的协程都执行完毕
    time.Sleep(5 * time.Second)
}

执行结果如下,可以看到总共 4 个任务,由于协程池大小为 2,所以 4 个任务分两批执行(从打印的时间可以看出)

2020-05-24 23:18:02.014487 +0800 CST m=+2.005207182
2020-05-24 23:18:02.014524 +0800 CST m=+2.005243650
2020-05-24 23:18:04.019755 +0800 CST m=+4.010435443
2020-05-24 23:18:04.019819 +0800 CST m=+4.010499440

系列导读

从零学习 Go 语言(01):一文搞定开发环境的搭建

从零学习 Go 语言(02):学习五种变量创建的方法

从零学习 Go 语言(03):数据类型之整型与浮点型

从零学习 Go 语言(04):byte、rune与字符串

从零学习 Go 语言(05):数据类型之数组与切片

从零学习 Go 语言(06):数据类型之字典与布尔类型

从零学习 Go 语言(07):数据类型之指针

从零学习 Go 语言(08):流程控制之if-else

从零学习 Go 语言(09):流程控制之switch-case

从零学习 Go 语言(10):流程控制之for 循环

从零学习 Go 语言(11):goto 无条件跳转

从零学习 Go 语言(12):流程控制之defer 延迟语句

从零学习 Go 语言(13):异常机制 panic 和 recover

从零学习 Go 语言(14):Go 语言中的类型断言是什么?

从零学习 Go 语言(15):学习 Go 语言的结构体与继承

从零学习 Go 语言(17):Go 语言中的 make 和 new 有什么区别?

从零学习 Go 语言(18):Go 语言中的 语句块与作用域

从零学习 Go 语言(19):Go Modules 前世今生及入门使用

从零学习 Go 语言(20):关于包导入必学的 8 个知识点

从零学习 Go 语言(21):一文了解 Go语言中编码规范

从零学习 Go 语言(22):Go 语言中如何开源自己写的包给别人用?

从零学习 Go 语言(23):一篇文章搞懂 Go 语言的函数

从零学习 Go 语言(24):理解 Go 语言中的 goroutine

从零学习 Go 语言(25):详解信道/通道

从零学习 Go 语言(26):通道死锁经典错误案例详解

从零学习 Go 语言(27):学习 Go 协程中的 WaitGroup

从零学习 Go 语言(28):学习 Go 协程中的互斥锁和读写锁

从零学习 Go 语言(29):Go 语言中的 select 用法

从零学习 Go 语言(30):如何使用 GDB 调试 Go 程序?

从零学习 Go 语言(31):Go 语言里的空接口

从零学习 Go 语言(32):理解 Go 语言中的 Context

从零学习 Go 语言(33):如何手动实现一个协程池?


以上所述就是小编给大家介绍的《从零学习 Go 语言(33):如何手动实现一个协程池?》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

大数据之路

大数据之路

阿里巴巴数据技术及产品部 / 电子工业出版社 / 2017-7-1 / CNY 79.00

在阿里巴巴集团内,数据人员面临的现实情况是:集团数据存储已经达到EB级别,部分单张表每天的数据记录数高达几千亿条;在2016年“双11购物狂欢节”的24小时中,支付金额达到了1207亿元人民币,支付峰值高达12万笔/秒,下单峰值达17.5万笔/秒,媒体直播大屏处理的总数据量高达百亿级别且所有数据都需要做到实时、准确地对外披露……巨大的信息量给数据采集、存储和计算都带来了极大的挑战。 《大数据......一起来看看 《大数据之路》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具