Java开发者的Golang进修指南:从0->1带你实现协程池
在Java编程中,为了降低开销和优化程序的效率,我们常常使用线程池来管理线程的创建和销毁,并尽量复用已创建的对象。这样做不仅可以提高程序的运行效率,还能减少垃圾回收器对对象的回收次数。
在Golang中,我们知道协程(goroutine)由于其体积小且效率高,在高并发场景中扮演着重要的角色。然而,资源始终是有限的,即使你的内存很大,也会有一个限度。因此,协程池在某些情况下肯定也会有其独特的适用场景。毕竟,世上并不存在所谓的银弹,只有在特定情况下才能找到最优的解决方案。因此,在Golang中,我们仍然需要考虑使用协程池的情况,并根据具体场景来选择最佳的解决方案。
今天,我们将从Java线程池的角度出发,手把手地带你实现一个Golang协程池。如果你觉得有些困难,记住我们的宗旨是使用固定数量的协程来处理所有的任务。这样做的好处是可以避免协程数量过多导致资源浪费,也能确保任务的有序执行。让我们一起开始,一步步地构建这个Golang协程池吧!
协程池
首先,让我们编写一个简单的多协程代码,并逐步进行优化,最终实现一个简化版的协程池。假如我们有10个任务需要处理。我们最简单做法就是直接使用go
关键字直接去生成相应的协程即可,比如这样:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(5)
for i := 1; i <= 5; i++ {
go func(index int) {
fmt.Printf("Goroutine %d started\n", index)
time.Sleep(1 * time.Second)
fmt.Printf("Goroutine %d finished\n", index)
wg.Done()
}(i)
}
wg.Wait()
fmt.Println("All goroutines finished")
}
我们当前的用法非常简单,无需专门维护goroutine容量大小。每当有一个任务出现,我们就创建一个goroutine来处理。现在,让我们进一步优化这种用法。
优化版协程池
现在我们需要首先创建一个pool对象和一个专门用于运行协程的worker对象。如果你熟悉Java中线程池的源码,对于worker这个名称应该不会陌生。worker是一个专门用于线程复用的对象,而我们的worker同样负责运行任务。
实体Job没啥好说的,就是我们具体的任务。我们先来定义一个简单的任务实体Job:
type Job struct {
id int
num int
}
我们来看下主角Worker定义:
type Worker struct {
id int
jobChannel chan Job
wg *sync.WaitGroup
}
func NewWorker(id int, wga *sync.WaitGroup) *Worker {
return &Worker{
id: id,
jobChannel: make(chan Job, 1),
wg: wga,
}
}
func (w *Worker) Start() {
go func() {
for job := range w.jobChannel {
fmt.Printf("Worker %d is processing job %d\n", w.id, job.id)
// 模拟任务处理
fmt.Println("jobs 模拟任务处理")
time.Sleep(2 * time.Second) // 休眠2秒钟
result := job.num * 2
fmt.Printf("Worker %d finished job %d, result: %d\n", w.id, job.id, result)
w.wg.Done()
}
}()
}
你可以看到,我的实体拥有一个channel。这个channel类似于我们想要获取的任务队列。与Java中使用链表形式并通过独占锁获取共享链表这种临界资源的方式不同,我选择使用了Golang中的channel来进行通信。因为Golang更倾向于使用channel进行通信,而不是共享资源。所以,我选择了使用channel。为了避免在添加任务时直接阻塞,我特意创建了一个带有任务缓冲的channel。
在这里,Start()
方法是一个真正的协程,我会从channel中持续获取任务,以保持这个协程不被销毁,直到没有任务可获取为止。这个逻辑类似于Java中的线程池。
让我们进一步深入地探讨一下Pool池的定义:
type Pool struct {
workers []*Worker
jobChan chan Job
wg sync.WaitGroup
}
func NewPool(numWorkers, jobQueueSize int) *Pool {
pl := &Pool{
workers: make([]*Worker, numWorkers),
jobChan: make(chan Job, jobQueueSize),
}
for i := 0; i < numWorkers; i++ {
pl.workers[i] = NewWorker(i+1, &pl.wg)
}
return pl
}
func (p *Pool) Start() {
for _, worker := range p.workers {
worker.Start()
}
go p.dispatchJobs()
}
func (p *Pool) dispatchJobs() {
for job := range p.jobChan {
worker := p.getLeastBusyWorker()
worker.jobChannel <- job
}
}
func (p *Pool) getLeastBusyWorker() *Worker {
leastBusy := p.workers[0]
for _, worker := range p.workers {
if len(worker.jobChannel) < len(leastBusy.jobChannel) {
leastBusy = worker
}
}
return leastBusy
}
func (p *Pool) AddJob(job Job) {
fmt.Println("jobs add")
p.wg.Add(1)
p.jobChan <- job
}
他的定义可能有点复杂,但是我可以用简单的语言向你解释,就是那些东西,只是用Golang的写法来实现,与Java的实现方式类似。
首先,我定义了一个名为worker的数组,用于存储当前存在的worker数量。这里并没有实现核心和非核心worker的区分。另外,我还创建了一个独立的channel,用于保存可缓冲任务的大小。这些参数在初始化时是必须提供的。
Start
方法的主要目的是启动worker开始执行任务。首先,它会启动一个核心协程,等待任务被派发。然后,dispatchJobs方法会开始监听channel是否有任务,如果有任务,则会将任务分派给worker进行处理。在整个过程中,通过channel进行通信,没有使用链表或其他共享资源实体。需要注意的是,dispatchJobs方法在另一个协程中被调用,这是为了避免阻塞主线程。
getLeastBusyWorker
方法是用来获取阻塞任务最少的worker的。这个方法的主要目标是保持任务平均分配。而AddJob
方法则是用来直接向channel中添加job任务的,这个方法比较简单,不需要过多解释。
协程池最终实现
经过一系列的反复修改和优化,我们终于成功实现了一个功能完善且高效的Golang协程池。下面是最终版本的代码:
package main
import (
"fmt"
"sync"
"time"
)
type Job struct {
id int
num int
}
type Worker struct {
id int
jobChannel chan Job
wg *sync.WaitGroup
}
func NewWorker(id int, wga *sync.WaitGroup) *Worker {
return &Worker{
id: id,
jobChannel: make(chan Job, 1),
wg: wga,
}
}
func (w *Worker) Start() {
go func() {
for job := range w.jobChannel {
fmt.Printf("Worker %d is processing job %d\n", w.id, job.id)
// 模拟任务处理
fmt.Println("jobs 模拟任务处理")
time.Sleep(2 * time.Second) // 休眠2秒钟
result := job.num * 2
fmt.Printf("Worker %d finished job %d, result: %d\n", w.id, job.id, result)
w.wg.Done()
}
}()
}
type Pool struct {
workers []*Worker
jobChan chan Job
wg sync.WaitGroup
}
func NewPool(numWorkers, jobQueueSize int) *Pool {
pl := &Pool{
workers: make([]*Worker, numWorkers),
jobChan: make(chan Job, jobQueueSize),
}
for i := 0; i < numWorkers; i++ {
pl.workers[i] = NewWorker(i+1, &pl.wg)
}
return pl
}
func (p *Pool) Start() {
for _, worker := range p.workers {
worker.Start()
}
go p.dispatchJobs()
}
func (p *Pool) dispatchJobs() {
for job := range p.jobChan {
worker := p.getLeastBusyWorker()
worker.jobChannel <- job
}
}
func (p *Pool) getLeastBusyWorker() *Worker {
leastBusy := p.workers[0]
for _, worker := range p.workers {
if len(worker.jobChannel) < len(leastBusy.jobChannel) {
leastBusy = worker
}
}
return leastBusy
}
func (p *Pool) AddJob(job Job) {
fmt.Println("jobs add")
p.wg.Add(1)
p.jobChan <- job
}
func main() {
pool := NewPool(3, 10)
pool.Start()
// 添加任务到协程池
for i := 1; i <= 15; i++ {
pool.AddJob(Job{
id: i,
num: i,
})
}
// 等待所有任务完成
pool.wg.Wait()
close(pool.jobChan)
fmt.Println("All jobs finished")
}
三方协程池
在这种场景下,既然已经有一个存在的场景,那么显然轮子是肯定有的。不论使用哪种编程语言,我们都可以探索一下,以下是Golang语言中关于三方协程池的实现工具。
ants
: ants是一个高性能的协程池实现,支持动态调整协程池的大小,可以通过简单的API调用来将任务提交给协程池进行执行。官方地址:https://github.com/panjf2000/ants
gopool
:gopool 是一个高性能的 goroutine 池,旨在重用 goroutine 并限制 goroutine 的数量。官方地址:https://github.com/bytedance/gopkg/tree/develop/util/gopool
这些库都提供了简单易用的API,可以方便地创建和管理协程池,同时也支持动态调整协程池的大小,以满足不同场景下的需求。
总结
当然,我写的简易版协程池还有很多可以优化的地方,比如可以实现动态扩容等功能。今天我们要简单总结一下协程池的优势,主要是为了降低资源开销。协程池的好处在于可以重复利用协程,避免频繁创建和销毁协程,从而减少系统开销,提高系统性能。此外,协程池还可以提高响应速度,因为一旦接收到任务,可以立即执行,不需要等待协程创建的时间。另外,协程池还具有增强可管理性的优点,可以对协程进行集中调度和统一管理,方便进行性能调优。
Java开发者的Golang进修指南:从0->1带你实现协程池的更多相关文章
- JAVA开发者的Golang快速指南
Golang作为Docker.Kubernetes和OpenShift等一些酷辣新技术的首选编程语言,越来越受欢迎.尤其它们都是开源的,很多情况下,开源是非常有价值的.深入学习阅Golang等源代码库 ...
- golang学习笔记20 一道考察对并发多协程操作一个共享变量的面试题
golang学习笔记20 一道考察对并发多协程操作一个共享变量的面试题 下面这个程序运行的能num结果是什么? package main import ( "fmt" " ...
- golang协程池设计
Why Pool go自从出生就身带“高并发”的标签,其并发编程就是由groutine实现的,因其消耗资源低,性能高效,开发成本低的特性而被广泛应用到各种场景,例如服务端开发中使用的HTTP服务,在g ...
- Golang协程池(workpool)实现
背景 因与工作相关,所以本文中的数据都进行了更改,但逻辑是一样的. 笔者的服务ServerA会请求服务ServerH获取一些数据,但ServerH的接口有个N秒内只能请求M次的限制,并返回false. ...
- golang协程池
type GoroutinePoll struct { Queue chan func() error Total, Num int Result chan error FinishCallBack ...
- Java协程编程之Loom项目尝鲜
前提 之前很长一段时间关注JDK协程库的开发进度,但是前一段时间比较忙很少去查看OpenJDK官网的内容.Java协程项目Loom(因为项目还在开发阶段,OpenJDK给出的官网https://ope ...
- 深入分析 Java、Kotlin、Go 的线程和协程
前言 协程是什么 协程的好处 进程 进程是什么 进程组成 进程特征 线程 线程是什么 线程组成 任务调度 进程与线程的区别 线程的实现模型 一对一模型 多对一模型 多对多模型 线程的"并发& ...
- golang源码阅读:VictoriaMetrics中的协程优先级的处理方式
在阅读VictoriaMetrics的源码的时候,读到了那么平平无奇的一段: // AddRows adds the given mrs to s. func (s *Storage) AddRows ...
- Java程序员的Golang入门指南(下)
Java程序员的Golang入门指南(下) 4.高级特性 上面介绍的只是Golang的基本语法和特性,尽管像控制语句的条件不用圆括号.函数多返回值.switch-case默认break.函数闭包.集合 ...
- Java程序员的Golang入门指南(上)
Java程序员的Golang入门指南 1.序言 Golang作为一门出身名门望族的编程语言新星,像豆瓣的Redis平台Codis.类Evernote的云笔记leanote等. 1.1 为什么要学习 如 ...
随机推荐
- Python给exe添加以管理员运行的属性
需求 有些应用每次启动都需要用管理员权限运行,比如Python注入dll时,编辑器或cmd就需要以管理员权限运行,不然注入就会失败. 这篇文章用编程怎么修改配置实现打开某个软件都是使用管理员运行,就不 ...
- 【笔记】springSecurity-OAuth2.0-授权模式演示
SpringSecurityOauth2架构 介绍 流程: 用户访问,此时没有Token.Oauth2RestTemplate会报错,这个报错信息会被Oauth2ClientContextFilter ...
- Hystrix:Spring Cloud服务熔断与降级组件
Hystrix:Spring Cloud服务熔断与降级组件 问题总结 熔断器? Spring Cloud Hystrix? Hystrix服务降级? 全局降级方法? 解耦降级逻辑? Hystrix服务 ...
- JavaFx之模态窗口(二十六)
JavaFx之模态窗口(二十六) 模态窗口:在场景A打开场景B,则A场景无法选择和操作,只能操作B 设置方式,在场景B初始化时设置 stage.initModality(Modality.APPLIC ...
- k8s 标签-2
目录 标签-2 node的角色 修改node节点的角色,将他的角色修改成他的主机名 标签的作用 Cordon,Drain以及污点 Cordon--告警警戒 Drain 驱逐演示 污点 污点的Cordo ...
- 2024年,在风云际会的编程世界里,窥探Java的前世今生,都说它穷途末路,我认为是柳暗花明!
2024年,在风云际会的编程世界里,窥探Java的前世今生,都说它穷途末路,我认为是柳暗花明! 文编|JavaBuild 哈喽,大家好呀!我是JavaBuild,以后可以喊我鸟哥,嘿嘿!俺滴座右铭是不 ...
- 这8个JS 新功能,你应该去尝试一下
摘要:本文主要介绍几个已经进入stage4的提案,这几个提案有望在2022年逐步纳入标准. 本文分享自华为云社区<2022 年你应该尝试的 8个 JavaScript 新功能>,作者:前端 ...
- CentOS7与centOS8的抉择
目前国内各大云服务器的默认centos 系统版本还是7,vultr,centos只有8了 官网,下载,默认也是8,作为本地主机玩的服务器,还是试一下centos8 国外下载之前版本,下载链接: 官网默 ...
- JAVA CRC8
Java CRC8 /** * CRC-8 * * <table width="400px" border="1" cellpadding="0 ...
- 阿里云CentOS数据盘挂载(磁盘扩容)
1. df -h Disk label type 值为dos表示MBR分区,值为gpt表示GPT分区. [root@iZuf66gcq71y5hlfv02w6aZ ~]# yum install -y ...