前言

令牌桶是一种常见用于控制速率的控流算法。原理于 Wikipedia 上描述如下:

每秒会有 r 个令牌被放入桶中,即每 1 / r 秒向桶中放入一个令牌。

一个桶最多可以存放 b 个令牌。当令牌被放入桶时,若桶已满,则令牌被直接丢弃。

当一个 n 字节的数据包抵达时,消耗 n 个令牌,然后放行之。

若桶中的令牌不足 n ,则该数据包要么被缓存要么被丢弃。

下面我们便根据上述描述,使用 Go 语言,基于多 goroutine ,来实现是一个并发安全的令牌桶。后述代码的完整实现的仓库地址在:https://github.com/DavidCai19... 。

基本设计

最基本的结构便是,定义一个令牌桶 struct ,该 struct 每一个新生成的令牌桶实例,各自带有一个 goroutine ,像守护进程一样以固定时间向实例桶中放入令牌:

type TokenBucket struct {

interval time.Duration // 时间间隔

ticker *time.Ticker // 定时器 timer

// ...

cap int64 // 桶总容量

avail int64 // 桶内现有令牌数

}

func (tb *TokenBucket) adjustDaemon() {

for now := range tb.ticker.C {

var _ = now

if tb.avail < tb.cap {

tb.avail++

}

}

}

func New(interval time.Duration, cap int64) *TokenBucket {

tb := &TokenBucket{

// ...

}

go tb.adjustDaemon()

return tb

}

该 struct 最终会提供以下 API :

TryTake(count int64) bool: 尝试从桶中取出 n 个令牌。立刻返回,返回值表示该次取出是否成功。

Take(count int64):尝试从桶中取出 n 个令牌,若当前桶中的令牌数不足,则保持等待,直至桶内令牌数量达标然后取出。

TakeMaxDuration(count int64, max time.Duration) bool:尝试从桶中取出 n 个令牌,若当前桶中的令牌数不足,则保持等待,直至桶内令牌数量达标然后取出。不过设置了一个超时时间 max ,若超时,则不再等待立刻返回,返回值表示该次取出是否成功。

Wait(count int64):保持等待直至桶内令牌数大于等于 n 。

WaitMaxDuration(count int64, max time.Duration) bool 保持等待直至桶内令牌数大于等于 n ,但设置了一个超时时间 max 。

TryTake: 一次性取出尝试

TryTake(count int64) bool 这样的一次性取出尝试,即可返回,实现起来最为简易。唯一需要注意的问题为当前我们在一个多 goroutine 环境下,令牌是我们的共享资源,为了防止竞争条件,最简单的解决方案即为存取都加上锁。Go 语言自带的 sync.Mutex 类提供了锁的实现。

type TokenBucket struct {

// ...

tokenMutex *sync.Mutex // 令牌锁

}

func (tb *TokenBucket) tryTake(count int64) bool {

tb.tokenMutex.Lock() // 检查共享资源,加锁

defer tb.tokenMutex.Unlock()

if count <= tb.avail {

tb.avail -= count

return true

}

return false

}

func (tb *TokenBucket) adjustDaemon() {

for now := range tb.ticker.C {

var _ = now

tb.tokenMutex.Lock() // 检查共享资源,加锁

if tb.avail < tb.cap {

tb.avail++

}

tb.tokenMutex.Unlock()

}

}

Take,TakeMaxDuration 等待型取出(尝试)

对于 Take(count int64) 和 TakeMaxDuration(count int64, max time.Duration) bool 这样的等待型取出(尝试),情况别就有所不同了:

由于这两个操作都是需要进行等待被通知,故原本的主动加锁检查共享资源的方案已不再适合。

由于可能存在多个正在等待的操作,为了避免混乱,我们需要有个先来后到,最早等待的操作,首先获取令牌。

我们可以使用 Go 语言提供的第二种共享多 goroutine 间共享资源的方式:channel 来解决第一个问题。channel 可以是双向的,完全符合我们需要被动通知的场景。而面对第二个问题,我们需要为等待的操作维护一个队列。这里我们使用的是 list.List 来模拟 FIFO 队列,不过值得留意的是,这样一来,队列本身也成了一个共享资源,我们也需要为了它,来配一把锁。

跟着上述思路,我们先来实现 Take(count int64) :

type TokenBucket struct {

// ...

waitingQuqueMutex: &sync.Mutex{}, // 等到操作的队列

waitingQuque: list.New(), // 列队的锁

}

type waitingJob struct {

ch chan struct{}

count int64

}

func (tb *TokenBucket) Take(count int64) {

w := &waitingJob{

ch: make(chan struct{}),

count: count,

}

tb.addWaitingJob(w) // 将 w 放入列队,需为队列加锁。

close(w.ch)

}

func (tb *TokenBucket) adjustDaemon() {

var waitingJobNow *waitingJob

for now := range tb.ticker.C {

var _ = now

tb.tokenMutex.Lock() // 检查共享资源,加锁

if tb.avail < tb.cap {

tb.avail++

}

element := tb.getFrontWaitingJob() // 取出队列头,需为队列加锁。

if element != nil {

if waitingJobNow == nil {

waitingJobNow = element.Value.(*waitingJob)

tb.removeWaitingJob(element) // 移除队列头,需为队列加锁。

}

if tb.avail >= waitingJobNow.need {

tb.avail -= waitingJobNow.count

waitingJobNow.ch

waitingJobNow = nil

}

}

tb.tokenMutex.Unlock()

}

}

接着我们来实现 TakeMaxDuration(count int64, max time.Duration) bool ,该操作的超时部分,我们可以使用 Go 自带的 select 关键字结合定时器 channel 来实现。并且为 waitingJob 加上一个标识字段来表明该操作是否已超时被弃用。由于检查弃用的操作会在 adjustDaemon 中进行,而标识弃用的操作会在 TakeMaxDuration 内的 select 中,为了再次避免竞争状态,我们将使用的令牌的操作从 adjustDaemon 内通过 channel 返回给 select 中,并阻塞,来避免了竞争条件并且享受了令牌锁的保护:

func (tb *TokenBucket) TakeMaxDuration(count int64, max time.Duration) bool {

w := &waitingJob{

ch: make(chan struct{}),

count: count,

abandoned: false, // 超时弃置标识

}

defer close(w.ch)

tb.addWaitingJob(w)

select {

case

tb.avail -= use

w.ch

return true

case

w.abandoned = true

return false

}

}

func (tb *TokenBucket) adjustDaemon() {

// ...

if element != nil {

if waitingJobNow == nil || waitingJobNow.abandoned {

waitingJobNow = element.Value.(*waitingJob)

tb.removeWaitingJob(element)

}

if tb.avail >= waitingJobNow.need && !waitingJobNow.abandoned {

waitingJobNow.ch

waitingJobNow = nil

}

}

// ...

}

最后

最后总结一些关键点:

对于共享资源的存取,要么使用锁,要么使用 channel ,视场景选择最好用的用之。

channel 可被动等待共享资源,而锁则使用十分简易。

异步的多个等待操作,可使用队列进行协调。

可以在锁的保护下,结合 channel 来对共享资源实现一个处理 pipeline ,结合两者优势,十分好用。

有疑问加站长微信联系(非本文作者)

c语言令牌桶原理,基于多 goroutine 实现令牌桶相关推荐

  1. c语言动态扫描原理,基于视觉暂留的动态扫描LED旋转屏

    2.3 红外解码 红外遥控器与电器之间的通信存在一个通信协议,一般是单向的通信协议.这个单向的通信协议称为红外遥控编码协议.本文使用NEC红外编码协议,当发射器按键按下后,即有遥控码发出,所按的键不同 ...

  2. 可能要用心学高并发核心编程,限流原理与实战,分布式令牌桶限流

    实战:分布式令牌桶限流 本节介绍的分布式令牌桶限流通过Lua+Java结合完成,首先在Lua脚本中完成限流的计算,然后在Java代码中进行组织和调用. 分布式令牌桶限流Lua脚本 分布式令牌桶限流Lu ...

  3. 基于c语言的编译原理课程设计,编译原理课程设计心得体会

    与<编译原理课程设计心得体会>相关的范文 本文由leishensc贡献 doc文档可能在WAP端浏览体验不佳.建议您优先选择TXT,或下载源文件到本机查看. 2008-2009 学年第二学 ...

  4. 令牌桶算法和漏桶算法python_限流之漏桶算法与令牌桶算法

    在开发高并发系统时有三把利器用来保护系统:缓存.降级和限流 缓存:缓存的目的是提升系统访问速度和增大系统处理容量 降级:降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降 ...

  5. 基于matlab的通信原理,基于Matlab的通信原理

    基于Matlab的通信原理Tag内容描述: 1.基于基于 MATLABMATLAB 的眼图仿真的眼图仿真 及其与通信实验箱之结果的比较及其与通信实验箱之结果的比较 摘要摘要 通信实验往往可以从硬件和软 ...

  6. java合一算法_Prolog语言的编译原理:合一算法

    Prolog语言的编译原理:合一算法 分类:软考 | 更新时间:2016-07-08| 来源:转载 Prolog是一种基于谓词演算的程序设计语言.Prolog是一种说明性语言,它的基本意思是程序员着重 ...

  7. R语言编写自定义函数基于ggsumarystats函数计算每个分组的统计值、自定义可视化分组分面条形图,并在X轴标签下方添加分组对应的统计值(样本数N、中位数median、四分位数的间距iqr)

    R语言编写自定义函数基于ggsumarystats函数计算每个分组的统计值.自定义可视化分组分面条形图,并在X轴标签下方添加分组对应的统计值(样本数N.中位数median.四分位数的间距iqr) 目录

  8. R语言使用subset函数基于组合逻辑筛选dataframe符合条件的数据行(select observations)、并指定需要保留的dataframe数据列或者字段

    R语言使用subset函数基于组合逻辑筛选dataframe符合条件的数据行(select observations).并指定需要保留的dataframe数据列或者字段 目录

  9. R语言splines包构建基于logistic回归的自然样条分析:南非心脏病数据集、非线性:基函数展开和样条分析、你简单分析的不重要特征,可能只是线性不显著、而非线性是显著的

    R语言splines包构建基于logistic回归的自然样条分析:南非心脏病数据集.非线性:基函数展开和样条分析.你简单分析的不重要特征,可能只是线性不显著.而非线性是显著的 目录

  10. R语言使用dplyr包基于因子变量(factor)将原dataframe拆分为每一个因子对应的单独数据集dataframe实战

    R语言使用dplyr包基于因子变量(factor)将原dataframe拆分为每一个因子对应的单独数据集dataframe实战 目录

最新文章

  1. 如何用技术搞好英俄翻译?
  2. 考前自学系列·计算机组成原理·补码定点加减运算和溢出判断,浮点数的加减运算,原码的乘法
  3. 编程实现有关SMS4的2个程序之——编程实现线性变换模块
  4. opengl加载显示3D模型blend类型文件
  5. 每天一道LeetCode-----判断一个数是否是happy number(每一位的平方和最终为1)
  6. 十二、实战启动页(一)
  7. ascii码java生成_Java 生成 ASCII 字符画 实现代码
  8. 一起谈.NET技术,asp.net控件开发基础(17)
  9. 顶部吸附_吸附脱附催化燃烧的工作原理
  10. 原生JS实现随机点名项目
  11. zb_system login.php,zblog后台登录地址怎么修改?
  12. 《OpenGL ES 3.x游戏开发(下卷)》一2.8 小结
  13. DT大数据梦工厂 第72,73讲
  14. 北大信科计算机考研科目,GitHub - 2584800190/kao_yan: 19年北大信科考研经验
  15. 【项目实战】Python基于Apriori关联规则算法实现商品零售购物篮分析
  16. [-Flutter 自组篇-] 圆形进度条
  17. java md5在线解密免费_Java MD5如何解密?
  18. k8s [kubelet-check] Initial timeout of 40s passed.解决方案
  19. 高考失利后选择出国留学,一年至少20万人民币到底值不值?
  20. 基于R语言、MaxEnt模型融合技术的物种分布模拟、参数优化方法、结果分析制图与论文写作

热门文章

  1. SpringBoot+MybatisPlus实现关联表查询
  2. 脚本工具之下载M3U8文件类型的完整视频
  3. DSP eCAP脉冲捕获实验
  4. 硬件电路设计之与非门触发器74HC30和74HCT20
  5. 开源.net 混淆器ConfuserEx
  6. 测试显示器使用时间的软件,解决方案:显示响应时间测试软件
  7. 一文带你理解URI 和 URL 有什么区别?
  8. Java之美[从蛮荒到撬动地球]之设计模式二
  9. AGND DGND PGND GND
  10. c语言程序设计会出现什么问题,计算机C语言程序设计过程中的常见问题分析和研究...