背景

因与工作相关,所以本文中的数据都进行了更改,但逻辑是一样的。

笔者的服务ServerA会请求服务ServerH获取一些数据,但ServerH的接口有个N秒内只能请求M次的限制,并返回false。而笔者的服务瞬时请求量远超M次,所以采用了协程池在收到103错误时,停止worker的运行N秒,然后再启动。

协程池的概念

协程池的相关概念:要有一个一定数量大小的池子(pool),池子里存储需要执行的任务(task),还要有若干个工作协程(worker)。

协程池要有启动,停止,睡眠的功能。

下面是从零开始记录一下思想过程和遇到的问题。

基础版

在此版本里,除了睡眠的功能,已经实现了一个基本的协程池。

// workpool.go
package workpool import (
"context"
"sync"
) type TaskFunc func() type Task struct {
f TaskFunc
} type WorkPool struct {
pool chan *Task
workerCount int stopCtx context.Context
stopCancelFunc context.CancelFunc
wg sync.WaitGroup
} func (t *Task) Execute() {
t.f()
} func New(workerCount, poolLen int) *WorkPool {
return &WorkPool{
workerCount: workerCount,
pool: make(chan *Task, poolLen),
}
} func (w *WorkPool) PushTask(t *Task) {
w.pool <- t
} func (w *WorkPool) PushTaskFunc(f TaskFunc) {
w.pool <- &Task{
f: f,
}
} func (w *WorkPool) work() {
for {
select {
case <-w.stopCtx.Done():
w.wg.Done()
return
case t := <-w.pool:
t.Execute()
}
}
} func (w *WorkPool) Start() *WorkPool {
w.wg.Add(w.workerCount)
w.stopCtx, w.stopCancelFunc = context.WithCancel(context.Background())
for i := 0; i < w.workerCount; i++ {
go w.work()
}
return w
} func (w *WorkPool) Stop() {
w.stopCancelFunc()
w.wg.Wait()
}

看起来没什么毛病,还挺简洁。其实不然...

下面的程序是创建一个容量为50的workpool,并将通过3个worker输出100个数字。

// workpool_test.go
package workpool import (
"fmt"
"sync"
"testing"
) func TestWorkPool_Start(t *testing.T) {
wg := sync.WaitGroup{}
wp := New(3, 50).Start()
lenth := 100
wg.Add(lenth)
for i := 0; i < lenth; i++ {
wp.PushTaskFunc(func() {
defer wg.Done()
fmt.Print(i, " ")
})
}
wg.Wait()
}

运行后输出结果如下:

50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 51 51 51 51 69 72 78 78 80 81 81 83 83 83 83 83 83 83 83 83 83 83 83 83 83 83 83 83 83 83 83 83 83 83 83 83 83 83 84 84 84 84 50 84
100 100 100 100 100 100 100 100 100 100 50 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 84 100 100 100

这和想象中的输出 0-99 相差甚远。

其原因在于闭包函数对于外部变量是引用的,所以在函数执行的时候,i的值早就已经改变了。下面是一个关于闭包的简单例子。

x := 1
f := func() {
println(x)
}
x = 2
x = 3
f() // 3

可以将 f() 的调用时机对应为协程池中的 t.Execute()。

解决闭包引用问题

既然是因为闭包引用导致的问题,那就不使用闭包了呗。

可以把参数传到函数内,但是因为并不知道将要执行的函数需要的参数个数及类型,所以只能是使用不定长的interface{}TaskFunc,在使用的时候进行断言。

以下仅列出改动部分:

// workpool.go
type TaskFunc func(args ...interface{}) type Task struct {
f TaskFunc
args []interface{}
} func (t *Task) Execute() {
t.f(t.args...)
} func (w *WorkPool) PushTaskFunc(f TaskFunc, args ...interface{}) {
w.pool <- &Task{
f: f,
args: args,
}
}

以下是测试程序:

// workpool_test.go
package workpool import (
"fmt"
"sync"
"testing"
) func TestWorkPool_Start(t *testing.T) {
wg := sync.WaitGroup{}
wp := New(3, 50).Start()
lenth := 100
wg.Add(lenth)
for i := 0; i < lenth; i++ {
wp.PushTaskFunc(func(args ...interface{}) {
defer wg.Done()
fmt.Print(args[0].(int), " ")
}, i)
}
wg.Wait()
}

输出内容如下:

0 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 2 1 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 26 48 49 51 52 53 54 55 56 50 58 59 57 61 62 63 64 65 66 25 68 6
9 70 71 72 73 67 75 76 77 74 79 78 81 82 83 84 60 86 87 88 89 90 91 92 85 94 95 96 97 98 99 80 93

虽然顺序是错乱的,但这是正常情况,闭包引用问题已解决。

添加睡眠功能

基于开头的应用场景,在任意一个被worker执行的任务收到ServerH的103错误后,要停止所有worker一段时间,因为再一直请求也没有意义。

这个版本已经与笔者正在使用的相差无几了

// workpool.go
package workpool import (
"context"
"fmt"
"sync"
"sync/atomic"
"time"
) type Flag int64 const (
FLAG_OK Flag = 1 << iota
FLAG_RETRY Flag = 1 << iota
) type TaskFunc func(w *WorkPool, args ...interface{}) Flag type Task struct {
f TaskFunc
args []interface{}
} type WorkPool struct {
pool chan *Task
workerCount int // stop相关
stopCtx context.Context
stopCancelFunc context.CancelFunc
wg sync.WaitGroup // sleep相关
sleepCtx context.Context
sleepCancelFunc context.CancelFunc
sleepSeconds int64
sleepNotify chan bool
} func (t *Task) Execute(w *WorkPool) Flag {
return t.f(w, t.args...)
} func New(workerCount, poolLen int) *WorkPool {
return &WorkPool{
workerCount: workerCount,
pool: make(chan *Task, poolLen),
sleepNotify: make(chan bool),
}
} func (w *WorkPool) PushTask(t *Task) {
w.pool <- t
} func (w *WorkPool) PushTaskFunc(f TaskFunc, args ...interface{}) {
w.pool <- &Task{
f: f,
args: args,
}
} func (w *WorkPool) work(i int) {
for {
select {
case <-w.stopCtx.Done():
w.wg.Done()
return
case <-w.sleepCtx.Done():
time.Sleep(time.Duration(w.sleepSeconds) * time.Second)
case t := <-w.pool:
flag := t.Execute(w)
if flag&FLAG_RETRY != 0 {
w.PushTask(t)
fmt.Printf("work %v PushTask,pool length %v\n", i, len(w.pool))
}
}
}
} func (w *WorkPool) Start() *WorkPool {
fmt.Printf("workpool run %d worker\n", w.workerCount)
w.wg.Add(w.workerCount + 1)
w.stopCtx, w.stopCancelFunc = context.WithCancel(context.Background())
w.sleepCtx, w.sleepCancelFunc = context.WithCancel(context.Background())
go w.sleepControl()
for i := 0; i < w.workerCount; i++ {
go w.work(i)
}
return w
} func (w *WorkPool) Stop() {
w.stopCancelFunc()
w.wg.Wait()
} func (w *WorkPool) sleepControl() {
fmt.Println("sleepControl start...")
for {
select {
case <-w.stopCtx.Done():
w.wg.Done()
return
case <-w.sleepNotify:
fmt.Printf("receive sleep notify start...\n")
w.sleepCtx, w.sleepCancelFunc = context.WithCancel(context.Background())
w.sleepCancelFunc()
fmt.Printf("sleepControl will star sleep %v s\n", w.sleepSeconds)
time.Sleep(time.Duration(w.sleepSeconds) * time.Second)
w.sleepSeconds = 0
fmt.Println("sleepControl was end sleep")
}
}
} func (w *WorkPool) SleepNotify(seconds int64) {
// 因为需要CAS操作,所以sleepSeconds没有采用time.Duration类型
// 成功设置后才发出通知
if atomic.CompareAndSwapInt64(&w.sleepSeconds, 0, seconds) {
fmt.Printf("sleepSeconds set %v\n", seconds)
w.sleepNotify <- true
}
}

下面的测试程序中,模拟了一下ServerH,其使用场景与笔者工作中大同小异。

// workpool_test.go
package workpool import (
"fmt"
"sync"
"testing"
"time"
) // 这里模拟ServerH服务的限流操作
var serverh = &server{max: 10, interval: 5} type server struct {
count int
max int
lasttime time.Time
interval int64
mu sync.Mutex
} func (s *server) Access(i int) bool {
now := time.Now() s.mu.Lock()
defer s.mu.Unlock() time.Sleep(100 * time.Millisecond) if s.lasttime.Unix() <= 0 || s.count >= s.max {
if now.After(s.lasttime) {
s.count = 1
s.lasttime = time.Unix(now.Unix()+s.interval, 0)
return true
}
fmt.Printf("Access false,i=%d \n", i)
return false
} else {
s.count++
fmt.Printf("Access true,i=%d s.count %d\n", i, s.count)
return true
}
} // 这里是笔者服务的逻辑
func TestWorkPool_Start(t *testing.T) {
wp := New(3, 100).Start()
for i := 0; i < 100; i++ {
time.Sleep(100 * time.Millisecond)
wp.PushTaskFunc(func(w *WorkPool, args ...interface{}) Flag {
if !serverh.Access(args[0].(int)) {
// 发送睡眠5秒的通知
w.SleepNotify(5)
// 此次未执行成功,要将该任务放回协程池
return FLAG_RETRY
}
return FLAG_OK
}, i)
}
time.Sleep(100 * time.Second)
}

输出内容如下:

workpool run 3 worker
sleepControl start...
Access true,i=1 s.count 2
Access true,i=2 s.count 3
Access true,i=3 s.count 4
Access true,i=4 s.count 5
Access true,i=5 s.count 6
Access true,i=6 s.count 7
Access true,i=7 s.count 8
Access true,i=8 s.count 9
Access true,i=9 s.count 10
Access false,i=10
sleepSeconds set 5
work 1 PushTask,pool length 0
receive sleep notify start...
sleepControl will star sleep 5 s
Access false,i=10
work 0 PushTask,pool length 1
Access false,i=10
work 0 PushTask,pool length 2
Access false,i=11
work 2 PushTask,pool length 3
Access false,i=12
work 1 PushTask,pool length 5
Access false,i=13
work 0 PushTask,pool length 6
Access false,i=14
work 0 PushTask,pool length 7
Access false,i=10
work 1 PushTask,pool length 8
Access false,i=15
work 1 PushTask,pool length 9
Access false,i=11
work 0 PushTask,pool length 11
Access false,i=12
work 0 PushTask,pool length 11
Access false,i=16
work 0 PushTask,pool length 12
sleepControl was end sleep
Access true,i=17 s.count 2
Access true,i=14 s.count 3
Access true,i=18 s.count 4
Access true,i=10 s.count 5
Access true,i=15 s.count 6
Access true,i=20 s.count 7
Access true,i=19 s.count 8
Access true,i=12 s.count 9
Access true,i=11 s.count 10
Access false,i=21
sleepSeconds set 5
work 0 PushTask,pool length 53
receive sleep notify start...
sleepControl will star sleep 5 s
Access false,i=16
work 1 PushTask,pool length 54
Access false,i=22
work 2 PushTask,pool length 55
Access false,i=23
work 0 PushTask,pool length 57
Access false,i=24
...........

待补充

重试次数的逻辑

Golang协程池(workpool)实现的更多相关文章

  1. golang协程池设计

    Why Pool go自从出生就身带“高并发”的标签,其并发编程就是由groutine实现的,因其消耗资源低,性能高效,开发成本低的特性而被广泛应用到各种场景,例如服务端开发中使用的HTTP服务,在g ...

  2. golang协程池

    type GoroutinePoll struct { Queue chan func() error Total, Num int Result chan error FinishCallBack ...

  3. 面试必问:Golang高阶-Golang协程实现原理

    引言 实现并发编程有进程,线程,IO多路复用的方式.(并发和并行我们这里不区分,如果CPU是多核的,可能在多个核同时进行,我们叫并行,如果是单核,需要排队切换,我们叫并发) 进程和线程的区别 进程是计 ...

  4. fasthttp中的协程池实现

    fasthttp中的协程池实现 协程池可以控制并行度,复用协程.fasthttp 比 net/http 效率高很多倍的重要原因,就是利用了协程池.实现并不复杂,我们可以参考他的设计,写出高性能的应用. ...

  5. 【爬虫小程序:爬取斗鱼所有房间信息】Xpath(协程池版)

    # 本程序亲测有效,用于理解爬虫相关的基础知识,不足之处希望大家批评指正 from gevent import monkey monkey.patch_all() from gevent.pool i ...

  6. golang协程同步的几种方法

    目录 golang协程同步的几种方法 协程概念简要理解 为什么要做同步 协程的几种同步方法 Mutex channel WaitGroup golang协程同步的几种方法 本文简要介绍下go中协程的几 ...

  7. python 协程池和pool.map用法

    一.问题描述 现在有一段代码,需要扫描一个网段内的ip地址,是否可以ping通. 执行起来效率太慢,需要使用协程. #!/usr/bin/env python # -*- coding: utf-8 ...

  8. Python与Golang协程异同

    背景知识 这里先给出一些常用的知识点简要说明,以便理解后面的文章内容. 进程的定义: 进程,是计算机中已运行程序的实体.程序本身只是指令.数据及其组织形式的描述,进程才是程序的真正运行实例. 线程的定 ...

  9. Golang协程实现流量统计系统(3)

    进程.线程.协程 - 进程:太重 - 线程:上下文切换开销太大 - 协程:轻量级的线程,简洁的并发模式 Golang协程:goroutine Hello world package main impo ...

随机推荐

  1. Java实现 蓝桥杯 算法提高 最大值路径

    试题 算法提高 最大值路径 资源限制 时间限制:1.0s 内存限制:256.0MB 问题描述 刷微博,编程序.如下图所示,@北京发布 提出了如下"头脑震荡"问题.对此问题做一般化描 ...

  2. java实现第三届蓝桥杯数量周期

    数量周期 [结果填空](满分9分) 复杂现象背后的推动力,可能是极其简单的原理.科学的目标之一就是发现纷繁复杂的自然现象背后的简单法则.爱因斯坦的相对论是这方面的典范例证. 很早的时候,生物学家观察某 ...

  3. java实现第六届蓝桥杯九数组分数

    九数组分数 九数组分数 1,2,3...9 这九个数字组成一个分数,其值恰好为1/3,如何组法? 下面的程序实现了该功能,请填写划线部分缺失的代码. public class A { public s ...

  4. tensorflow2.0学习笔记第二章第三节

    2.3激活函数sigmoid函数 f(x)= 1/(1 + e^-x)tf.nn.sigmoid(x)特点:(1)求导后的数值在0-0.25之间,链式相乘之后容易使得值趋近于0,形成梯度消失 (2)输 ...

  5. k8s学习-资源清单

    4.kubernetes使用 4.1.资源清单 api 文档.api 描述 4.1.2.说明 必须存在的属性 主要的对象 额外的参数项 例子 vim my-app.yml apiVersion: v1 ...

  6. Vue可响应式数组方法

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  7. RocketMQ系列(四)顺序消费

    折腾了好长时间才写这篇文章,顺序消费,看上去挺好理解的,就是消费的时候按照队列中的顺序一个一个消费:而并发消费,则是消费者同时从队列中取消息,同时消费,没有先后顺序.RocketMQ也有这两种方式的实 ...

  8. javaCV开发详解之12:视频转apng动态图片实现,支持透明通道,也支持摄像机、桌面屏幕、流媒体等视频源转apng动态图

    wjavaCV系列文章: javacv开发详解之1:调用本机摄像头视频 javaCV开发详解之2:推流器实现,推本地摄像头视频到流媒体服务器以及摄像头录制视频功能实现(基于javaCV-FFMPEG. ...

  9. 果然学习好是有道理的,学习Mysql与正则表达式笔记

    正则表达式是用来匹配文本的特殊的字符集合,将一个正则表达式与文本串进行比较,Mysql中用where子句提供支持,正则表达式关键字:regexp1.使用‘|’匹配两个串中的一个 2.使用‘[]’匹配几 ...

  10. react 使用的方法:

    react  使用方法: 第一步: 初始化react 项目 (1)安装node npm (2)npm install --global create-react-app (3)create-react ...