Posted in

从 v1 到 v2:分享我 WorkQueue 项目重构的完整思路_AI阅读总结 — 包阅AI

包阅导读总结

1.

关键词:WorkQueue 、重构 、优化 、排序堆 、接口设计

2.

总结:作者分享了对 WorkQueue 项目的重构思路,指出 v1 版存在诸多问题,分析痛点后明确预期目标和思考顺序,最终通过重新设计排序堆、接口等实现优化。

3.

主要内容:

– WorkQueue 项目重构背景

– 随着用户增多,v1 版出现性能、设计、实现等问题。

– 其他业务组在处理延迟订单时出现顺序不准确等问题。

– 痛点分析

– 排序堆不稳定且性能差。

– 接口设计混乱。

– 功能不符合预期。

– 代码质量低。

– 架构设计混乱。

– 预期目标

– 排序堆稳定高效。

– 接口通用易用。

– 功能符合预期。

– 提升代码质量。

– 设计清晰架构。

– 思考过程

– 解决问题有顺序,先排序堆,再接口设计,接着功能问题,最后代码质量。

– 重构和优化

– 选择红黑树实现排序堆,使用链表避免内存碎片,利用 sync.Pool 提升性能。

思维导图:

文章地址:https://juejin.cn/post/7395969637877743670

文章来源:juejin.cn

作者:路口IT大叔_KUMA

发布时间:2024/7/28 8:59

语言:中文

总字数:14526字

预计阅读时间:59分钟

评分:84分

标签:开源项目,技术重构,代码优化,架构设计,红黑树


以下为原文内容

本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com

点击公众关注号,“技术干货”及时达!

我是 LEE,老李,一个在 IT 行业摸爬滚打 17 年的技术老兵。

有一段时间没有写文章了,主要是最近忙于技术分享和技术实践,没有时间写文章。同时,我也收到了一些使用我开源项目的用户的反馈,因此我一直在不断改进我的开源项目。今天正好没有太多事情,所以决定与大家分享我最近对 WorkQueue 项目进行的优化和重构的思路和实现细节。请注意,本文主要是关于之前的 WorkQueue 项目的优化和重构。如果您对 WorkQueue 项目不熟悉,可以先阅读我之前的文章:《简约而不简单:WorkQueue 的轻量级高效之道》。

在开始之前,先分享一下最近 WorkQueue 项目的情况。随着项目的用户增多,一些用户提出了一些问题,例如性能问题、设计问题和实现细节问题等。其中,对于排序堆的争议最大,一些用户认为排序堆的实现过于粗糙、不稳定、不够高效和灵活。我总结了一些常见问题,主要包括以下几点:

  1. 「排序堆」:使用传统的四叉堆实现,与二叉堆类似,但存在不能稳定保持堆序的问题,导致在处理大量数据时性能下降严重。
  2. 「接口设计」:项目的接口设计整体上比较混乱,不容易理解,内部实现也较为复杂,不够简洁。
  3. 「功能问题」:PriorityQueue 的实现与传统的 PriorityQueue 不太一样,导致一些用户在使用时遇到问题。
  4. 「代码质量」:项目中存在大量冗余代码、堆砌代码和硬编码等低级问题。
  5. 「架构设计」:各种 Queue 的继承和依赖关系比较混乱,缺乏清晰的 UML 类图,导致一些用户阅读源码时困难重重。

为了解决这些问题,我决定对 WorkQueue 项目进行全面的重构和优化。这次重构和优化不仅仅是简单地修改代码,而是对整个项目进行全面提升,包括架构设计、接口设计、代码质量和性能优化等方面。重构和优化的目的不是追求完美,而是提升项目的质量、性能、易用性和可维护性。

此外,这次重构和优化也是为了解决我自己的实际问题。因为我的许多开源项目都依赖于 WorkQueue 项目,所以我希望通过这次重构和优化,提升我的开源项目的质量和可维护性。同时,我也希望能够为大家提供一个更好的开源项目,使大家在使用时更加方便、高效和稳定。

事件背景

万物皆可故事,这次重构和优化的背景也是从一个故事开始的。

主要爆雷的地方来自我们其他业务组。他们反映在处理延迟订单时,使用老版本的 WorkQueue(归档为 「v1」 版)出现了一些问题。部分订单的处理时间点靠后的订单(如:2023/3/1 23:0:59)会莫名其妙地在处理时间点靠前的订单(如:2023/3/1 23:0:15)之前被处理,导致订单处理的顺序不准确,产生了不可预期的结果。

当初我以为是业务方的代码问题,并没有太过关注。这个问题只是影响了审计的结果,对业务本身并没有太大的影响。这个问题就此搁置了。然而,不巧的是,这个问题又出现了,这次是来自一个 WorkQueue 的使用者。他们也是在处理延迟任务时使用 WorkQueue,出现了类似的问题。这个问题就比较严重了,直接影响到了业务的正确性,导致了一些不可预期的结果。

另外一个问题是,在提交处理任务时,WorkQueue 会因为之前版本(「v1」 版)排序堆的实现问题以及与大家传统理解的不一致,导致一些使用者在使用时遇到问题。这严重影响了开发者对最终结果的预期。当然,这个问题也得到了外部 WorkQueue 的使用者的反馈。

当我完成 WorkQueue 「v2」 的开发和测试后,回头看之前的第一版 WorkQueue,我意识到当时的设计确实有些草率,虽然在一些地方注意了内存和性能设计,但整体项目和功能设计还是相对粗糙的,甚至存在一些问题。因此,这次重构和优化的目的就是为了解决这些问题。

痛点分析

在开始重构和优化之前,我们先来分析一下 WorkQueue 项目存在的一些痛点,主要有以下几个:

  1. 「排序堆」:需要设计一个高效稳定的排序堆,以保证堆序的稳定性并提升堆的性能。
  2. 「接口设计」:需要设计一个简洁易用的接口,以提升项目的易用性。
  3. 「功能问题」:需要设计一个符合大家理解的 PriorityQueue,以提升项目的易用性。
  4. 「代码质量」:需要提升项目的代码质量,以提高可维护性。
  5. 「架构设计」:需要设计一个清晰简洁的架构,以提升项目的可维护性。

考虑到老版本的 WorkQueue(归档为 「v1」 版)存在的问题较多,我认为可以将其归档,而不是基于该版本进行优化和重构。因此,我决定重新设计版本为 「v2」 的全新 WorkQueue 项目。

预期目标

1. 排序堆

为了实现稳定排序和优化整个 WorkQueue 项目的性能,排序堆需要满足以下要求:

  1. 保证堆序的稳定性,不受堆大小和传入数据变化的影响。
  2. 提供高效的堆排序算法,不受堆中元素或插入顺序变化的影响。

「预期验证代码:」

创建一个堆,随机插入一些数据,然后多次执行堆排序,验证排序结果的正确性。

funcHeapPush_Random(){
h:=New()
count:=4

//Pushthenodes
h.Push(&lst.Node{Priority:int64(2)})
h.Push(&lst.Node{Priority:int64(0)})
h.Push(&lst.Node{Priority:int64(1)})
h.Push(&lst.Node{Priority:int64(3)})

//Prrintthetreeorder
PrintRootIndexs(h)
PrintOrderTraversalIndexs(h.root)
}

2. 接口设计

为了实现一个通用且易于管理的接口,需要满足以下要求:

  1. 使用接口 Interface 的方式,为使用者提供通用接口,方便扩展。
  2. 使用接口 Interface 约束对外暴露的接口,方便统一管理。
  3. 使用接口 Interface 的继承方式,根据整个项目的设计,使接口层次更加清晰。
  4. 在变量中使用接口 Interface ,使整个项目的内部代码更加简洁且结果更可预期。

「预期验证代码:」

PriorityQueue 为例,创建一个 PriorityQueue 实例,并使用接口 Interface 的方式定义配置参数、回调参数以及继承底层接口。

「PriorityQueue 结构体」

直接使用实现了 Queue 接口的结构体作为 PriorityQueue 的继承,并实现了 PriorityQueue 的接口。

//PriorityQueue接口继承了Queue接口,并添加了一个PutWithPriority方法,用于将元素按优先级放入队列。
//ThePriorityQueueinterfaceinheritsfromtheQueueinterfaceandaddsaPutWithPrioritymethodtoputanelementintothequeuewithpriority.
typePriorityQueue=interface{
Queue

//PutWithPriority方法用于将元素按优先级放入队列。
//ThePutWithPrioritymethodisusedtoputanelementintothequeuewithpriority.
PutWithPriority(valueinterface{},priorityint64)error

//HeapRange方法用于遍历sorted堆中的所有元素。
//TheHeapRangemethodisusedtotraverseallelementsinthesortedheap.
HeapRange(fnfunc(valueinterface{},delayint64)bool)
}

//priorityQueueImpl结构体,实现了PriorityQueue接口
//ThepriorityQueueImplstruct,whichimplementsthePriorityQueueinterface
typepriorityQueueImplstruct{
//Queue是一个队列接口
//Queueisaqueueinterface
Queue

...
}

「PriorityQueue 配置参数」

使用 PriorityQueueConfig 结构体作为 PriorityQueue 的配置参数。PriorityQueueConfig 结构体继承了 QueueConfig 结构体,并添加了一个 callback 参数,用于回调函数。

//PriorityQueueConfig结构体,用于配置优先队列
//ThePriorityQueueConfigstruct,usedforconfiguringthepriorityqueue
typePriorityQueueConfigstruct{
//QueueConfig是队列的配置
//QueueConfigistheconfigurationofthequeue
QueueConfig

//callback是优先队列的回调函数
//callbackisthecallbackfunctionofthepriorityqueue
callbackPriorityQueueCallback
}

「PriorityQueue 回调函数」

使用 PriorityQueueCallback 接口作为 PriorityQueue 的回调函数。PriorityQueueCallback 接口继承了 QueueCallback 接口,并添加了一个 OnPriority 方法,用于在元素按优先级放入队列时被调用。

//PriorityQueueCallback接口继承了QueueCallback接口,并添加了OnPriority方法。
//ThePriorityQueueCallbackinterfaceinheritsfromtheQueueCallbackinterfaceandaddstheOnPrioritymethod.
typePriorityQueueCallback=interface{
QueueCallback

//OnPriority方法在元素被按优先级放入队列时被调用。
//TheOnPrioritymethodiscalledwhenanelementisputintothequeuewithpriority.
OnPriority(valueinterface{},priorityint64)
}

「PriorityQueue 创建方法」

创建一个新的 PriorityQueue,并返回一个实现了 PriorityQueue 接口的对象。

//NewPriorityQueue函数用于创建一个新的PriorityQueue
//TheNewPriorityQueuefunctionisusedtocreateanewPriorityQueue
funcNewPriorityQueue(config*PriorityQueueConfig)PriorityQueue{
...
}

3. 功能问题

为解决设计与预期不一致的问题,我们需要重新实现符合预期的 PriorityQueueDelayingQueue。具体要求如下:

  1. 「功能一致性」:确保 PriorityQueue 严格按照优先级处理元素。
  2. 「延迟处理」:确保 DelayingQueue 根据设定的延迟时间处理元素。
  3. 「错误处理」:增强异常处理能力,确保队列在各种边界条件下正常工作。

4. 代码质量

为提升代码质量,我们需关注以下方面:

  1. 「可维护性」:采用模块化设计和清晰的代码结构,使代码易于理解和修改。
  2. 「易读性」:保持一致的编码风格和清晰的命名规范,使代码易读易懂。
  3. 「可测试性」:编写单元测试和集成测试,确保代码的正确性和稳定性。
  4. 「避免硬编码」:使用配置文件和参数化设计,减少硬编码,提升代码灵活性。
  5. 「简洁高效」:通过重构和优化,减少冗余代码,提高执行效率。

5. 架构设计

为提升项目可维护性,我们将设计一个清晰简洁的架构,具体步骤如下:

  1. 「需求分析」:详细分析项目需求,明确各模块的功能和关系。
  2. 「类图设计」:根据需求分析,设计 UML 类图,明确各类的职责和关系。
  3. 「模块划分」:将项目划分为低耦合高内聚的多个模块。
  4. 「接口定义」:为各模块定义清晰的接口,确保模块间的通信和协作。
  5. 「实现和测试」:逐步实现各模块,并编写测试用例,确保模块的正确性和稳定性。

通过以上步骤,我们可以确保项目架构清晰简洁,代码质量高,功能实现符合预期。

思考过程

针对上面的痛点分析,我认为解决这些问题存在非常重要的顺序,并且这个顺序不宜颠倒。经过反复思考,我认为解决这些问题的顺序应该是:

  1. 「排序堆」:首先解决排序堆的问题,因为排序堆是整个项目的核心,也是整个项目的性能瓶颈,所以首先解决排序堆的问题。
  2. 「接口设计」:其次解决接口设计的问题,因为接口设计是整个项目的基础,也是整个项目的灵魂,所以其次解决接口设计的问题。
  3. 「功能问题」:再次解决功能问题,因为功能问题是整个项目的核心功能,也是整个项目的使用者关心的问题,所以再次解决功能问题。
  4. 「代码质量」:最后解决代码质量的问题,因为代码质量是整个项目的基础,也是整个项目的可维护性,所以最后解决代码质量的问题。

当然,以上只是整体思路。在解决了「排序堆」的问题后,可以使用「接口设计」来完成 UML 类图的设计,然后根据 UML 类图来实现整个项目的架构和功能。在解决了「功能问题」后,可以使用「代码质量」来提升项目的代码质量和可维护性。

重构和优化

有了上面的分析,现在可以开始重构和优化 WorkQueue 项目了。主要分为以下几个步骤:

  1. 「排序堆」:设计一个高效稳定的排序堆,保证堆序的稳定性并提升堆的性能。
  2. 「设计接口」:设计一个简洁易用的接口,提升项目的易用性。
  3. 「设计功能」:设计合理的功能,并编写更多的测试用例,提升项目的可靠性。

1. 排序堆

要优化排序堆,主要是对排序堆的实现进行优化,包括提升排序堆的性能和保证堆序的稳定性。在选择排序算法之前,我们先来看一下排序堆的实现。

1.1. 排序堆的种类

实际上,研究排序堆的种类就是研究排序算法。下面列举了一些常见的排序算法及其特点:

「常见排序算法对比表」

排序算法 优点 缺点 最佳时间复杂度 平均时间复杂度 最差时间复杂度 空间复杂度 排序结果 额外空间
冒泡排序 实现简单,适用于小数据集 效率低,特别是对于大数据集 O(n) O(n^2) O(n^2) O(1) 稳定
选择排序 实现简单,不需要额外的内存 效率低,特别是对于大数据集 O(n^2) O(n^2) O(n^2) O(1) 不稳定
插入排序 对于几乎已经排序的数据效率高,简单易实现 效率低,对于大数据集不适用 O(n) O(n^2) O(n^2) O(1) 稳定
归并排序 稳定,适用于大数据集 需要额外的内存 O(nlogn) O(nlogn) O(nlogn) O(n) 稳定
快速排序 平均情况下效率高 最差情况可能退化为 O(n^2) O(nlogn) O(nlogn) O(n^2) O(logn) 不稳定
堆排序 不需要额外的内存,适用于大数据集 实现复杂,不稳定 O(nlogn) O(nlogn) O(nlogn) O(1) 不稳定
希尔排序 适用于中等大小数据集,效率较高 不稳定,时间复杂度分析复杂 O(nlog^2n) O(nlog^2n) O(n^2) O(1) 不稳定
基数排序 对特定类型的数据非常高效 需要额外的内存,不适用于所有类型的数据 O(nk) O(nk) O(nk) O(n + k) 稳定
AVL 树 自平衡,查找、插入和删除操作时间复杂度低 只要节点变动都需要重新平衡,整个树趋近于平衡 O(logn) O(logn) O(logn) O(n) 稳定(*)
红黑树 自平衡,查找、插入和删除操作时间复杂度低 插入节点树发生重平衡,整个树存在一定的不平衡 O(logn) O(logn) O(logn) O(n) 稳定(*)

需要注意的是,表中将 AVL 树红黑树 也列入了排序算法对比表中。虽然 AVL 树红黑树 是一种基于树的排序算法,但它们也可以满足我们对堆的稳定性的需求。

1.2. 排序堆的实现

在排序堆的实现中,我选择了使用 红黑树 作为排序堆的实现。红黑树 是一种自平衡二叉搜索树,可以保证堆序的稳定性,并且具有较好的性能。尽管 红黑树 的实现较为复杂,但我认为这个实现是值得的,因为它可以保证堆序的稳定性并提升堆的性能。

「注意」AVL 树红黑树 并非能满足严格意义上的对序堆肯定性严格意义,但是它们能够保证在堆内数值的严格排序性,这个是满足我们的设计需要的。

有人可能会问,为什么不使用 go 官方的 container/heap 包呢?这是因为 container/heap 包是基于「数组」实现的,而我需要的是基于「链表」实现的堆。因此,我只能自己实现一个基于「链表」的排序堆。

(★) 内存碎片化对比

在大多数编程语言中,使用 new 关键字在「堆内存」中分配对象。

特性 栈内存 堆内存
「碎片化原因」 按顺序分配和释放,不会产生碎片 随机分配和释放,可能产生碎片
「碎片化表现」 内存空洞,影响大块内存分配
「应对策略」 不需要 内存池、紧凑化、最佳适配算法等

根据上表,我们可以看出,「堆内存」可能会产生碎片,而「栈内存」不会产生碎片。如果使用「数组」作为「堆」的实现,那么在「堆」的插入和删除操作中,可能会产生大量的内存碎片,导致内存空洞,影响大块内存分配。因此,我选择使用「链表」作为「堆」的实现,因为「链表」可以充分利用内存中的空间,不会产生内存碎片。我在之前的文章《简约而不简单:WorkQueue 的轻量级高效之道》中详细介绍了这一点。

在排序堆的实现中,我还使用了 go 中的 sync.Pool 来复用堆中的节点对象,以减少内存分配和垃圾回收的次数,提升堆的性能。

「示意图」

示意图仅仅是一个简单的示意图,并不能完全覆盖 红黑树 的插入的所有情况和完整步骤。

wqv2-2.png

「演示代码」

  • 插入节点

    //insert方法用于向红黑树中插入一个节点
    //TheinsertmethodisusedtoinsertanodeintotheRED-BLACKtree
    func(tree*RBTree)insert(node*lst.Node){

    //parent用于保存当前节点的父节点
    //parentisusedtosavetheparentofthecurrentnode
    varparent*lst.Node

    //current用于保存当前正在处理的节点,初始为树根
    //currentisusedtosavethenodecurrentlybeingprocessed,initiallytherootofthetree
    current:=tree.root

    //当current不为空时,进行循环
    //Loopwhencurrentisnotnil
    forcurrent!=nil{

    //将parent设置为current
    //Setparenttocurrent
    parent=current

    //如果node的优先级小于current的优先级
    //Ifthepriorityofnodeislessthanthepriorityofcurrent
    ifnode.Priority<current.Priority{

    //将current设置为其左子节点
    //Setcurrenttoitsleftchild
    current=current.Left

    }else{

    //将current设置为其右子节点
    //Setcurrenttoitsrightchild
    current=current.Right

    }

    }

    //将node的父节点设置为parent
    //Settheparentofnodetoparent
    node.Parent=parent

    //如果parent为空
    //Ifparentisnil
    ifparent==nil{

    //将树根设置为node
    //Settherootofthetreetonode
    tree.root=node

    }elseifnode.Priority<parent.Priority{//如果node的优先级小于parent的优先级

    //将parent的左子节点设置为node
    //Settheleftchildofparenttonode
    parent.Left=node

    }else{

    //将parent的右子节点设置为node
    //Settherightchildofparenttonode
    parent.Right=node

    }

    //将node的左子节点和右子节点都设置为nil
    //Setboththeleftchildandrightchildofnodetonil
    node.Left=nil
    node.Right=nil

    //将node的颜色设置为红色
    //SetthecolorofnodetoRED
    node.Color=lst.RED

    //调用insertFixUp方法对树进行调整
    //CalltheinsertFixUpmethodtoadjustthetree
    insertFixUp(tree,node)

    //将树的节点数量加一
    //Increasethenumberofnodesinthetreebyone
    tree.count++

    //如果树的头节点为空或node的优先级小于头节点的优先级
    //Iftheheadofthetreeisnilorthepriorityofnodeislessthanthepriorityofthehead
    iftree.head==nil||node.Priority<tree.head.Priority{

    //将树的头节点设置为node
    //Settheheadofthetreetonode
    tree.head=node

    }

    //如果树的尾节点为空或node的优先级大于尾节点的优先级
    //Ifthetailofthetreeisnilorthepriorityofnodeisgreaterthanthepriorityofthetail
    iftree.tail==nil||node.Priority>tree.tail.Priority{

    //将树的尾节点设置为node
    //Setthetailofthetreetonode
    tree.tail=node

    }

    }
  • 删除节点

    //delete函数用于删除红黑树中的节点
    //ThedeletefunctionisusedtodeleteanodeintheRED-BLACKtree
    func(tree*RBTree)delete(node*lst.Node){

    //定义child和target节点
    //Definechildandtargetnodes
    varchild,target*lst.Node

    //如果node的左子节点为空或右子节点为空
    //Iftheleftchildofnodeisnilortherightchildisnil
    ifnode.Left==nil||node.Right==nil{

    //target设置为node
    //Settargettonode
    target=node

    }else{

    //target设置为node的后继节点
    //Settargettothesuccessorofnode
    target=tree.successor(node)

    }

    //如果target的左子节点不为空
    //Iftheleftchildoftargetisnotnil
    iftarget.Left!=nil{

    //child设置为target的左子节点
    //Setchildtotheleftchildoftarget
    child=target.Left

    }else{

    //child设置为target的右子节点
    //Setchildtotherightchildoftarget
    child=target.Right

    }

    //如果child不为空
    //Ifchildisnotnil
    ifchild!=nil{

    //将child的父节点设置为target的父节点
    //Settheparentofchildtotheparentoftarget
    child.Parent=target.Parent

    }

    //如果target的父节点为空
    //Iftheparentoftargetisnil
    iftarget.Parent==nil{

    //将树根设置为child
    //Settherootofthetreetochild
    tree.root=child

    }elseiftarget==target.Parent.Left{//如果target是其父节点的左子节点

    //将target的父节点的左子节点设置为child
    //Settheleftchildoftheparentoftargettochild
    target.Parent.Left=child

    }else{//如果target是其父节点的右子节点

    //将target的父节点的右子节点设置为child
    //Settherightchildoftheparentoftargettochild
    target.Parent.Right=child

    }

    //如果target不等于node
    //Iftargetisnotequaltonode
    iftarget!=node{

    //将node的值设置为target的值
    //Setthevalueofnodetothevalueoftarget
    node.Value=target.Value

    //将node的优先级设置为target的优先级
    //Setthepriorityofnodetothepriorityoftarget
    node.Priority=target.Priority

    }

    //如果target的颜色为黑色
    //IfthecoloroftargetisBLACK
    iftarget.Color==lst.BLACK{

    //调用deleteFixUp函数调整红黑树
    //CallthedeleteFixUpfunctiontoadjusttheRED-BLACKtree
    deleteFixUp(tree,child)

    }

    //将树的节点数量减一
    //Decreasethenumberofnodesinthetreebyone
    tree.count--

    //如果树的节点数量为零
    //Ifthenumberofnodesinthetreeiszero
    iftree.count==0{

    //将树的头节点和尾节点设置为nil
    //Settheheadandtailnodesofthetreetonil
    tree.head=nil
    tree.tail=nil

    }else{

    //如果树的头节点等于node
    //Iftheheadnodeofthetreeisequaltonode
    iftree.head==node{

    //将树的头节点设置为树根的最小节点
    //Settheheadnodeofthetreetotheminimumnodeoftheroot
    tree.head=tree.minimum(tree.root)

    }

    //如果树的尾节点等于node
    //Ifthetailnodeofthetreeisequaltonode
    iftree.tail==node{

    //将树的尾节点设置为树根的最大节点
    //Setthetailnodeofthetreetothemaximumnodeoftheroot
    tree.tail=tree.maximum(tree.root)

    }

    }

    }

1.3. 排序堆的优化经历

当然,这个选择过程并不是一帆风顺的。在选择使用 红黑树 作为 排序堆 的实现之前,我也考虑了其他的排序算法,比如插入排序AVL 树等。现在我来详细说明一下放弃 插入排序AVL 树 的原因。

插入排序

插入排序 是一种简单直观的排序算法,但是它的时间复杂度是 O(n^2),不适合处理大数据集。虽然 插入排序 在处理小规模数据时性能较高,但是在处理大规模数据时性能较低。因此,我决定放弃 插入排序 算法。

排序堆的实现中,有一个循环会从 链表 的最后一个节点开始遍历。这个循环的目的是找到合适的位置来插入新的节点。

「示意图」

wqv2-3.png

「演示代码」

//Push方法将一个新的节点添加到堆中。
//ThePushmethodaddsanewnodetotheheap.
func(h*Heap)Push(n*lst.Node){
//如果n是nil或者堆的长度为0,我们就直接在链表的后面添加n。
//Ifnisnilorthelengthoftheheapis0,wejustaddntothebackofthelist.
ifn==nil||h.list.Len()==0{
h.list.PushBack(n)
return
}

//获取链表的最后一个节点。
//Getthelastnodeofthelist.
current:=h.list.Back()

//如果当前节点不是nil并且n小于当前节点,我们就继续向前查找。
//Ifthecurrentnodeisnotnilandnislessthanthecurrentnode,wecontinuetolookforward.
forcurrent!=nil&&h.less(n,current){
current=current.Prev
}

//如果当前节点是nil,我们就在链表的前面添加n。
//Ifthecurrentnodeisnil,weaddntothefrontofthelist.
ifcurrent==nil{
h.list.PushFront(n)
}else{
//否则,我们就在当前节点的后面添加n。
//Otherwise,weaddnafterthecurrentnode.
h.list.InsertAfter(n,current)
}
}

AVL 树

AVL 树 是一种自平衡二叉搜索树,它能够保持堆序的稳定性,并且具有良好的性能。然而,AVL 树 的实现相对复杂,而且在树发生变化时需要进行复杂的平衡操作。无论是插入节点还是删除节点,都会触发这些平衡操作。因此,AVL 树 的性能相对较低。考虑到这一点,我决定不使用 AVL 树

「示意图」

「演示代码」

//insert插入新节点
//insertinsertsanewnode
func(t*SortingTree)insert(node*Node,itemItem)*Node{
//如果节点为空,创建一个新节点并返回
//Ifthenodeisnil,createanewnodeandreturnit
ifnode==nil{
return&Node{item:item,height:1}
}

//如果插入项的值小于当前节点的值,或者值相等但序列号小于当前节点的序列号,则插入到左子树
//Iftheitem'svalueislessthanthecurrentnode'svalue,orthevaluesareequalbutthesequencenumberisless,insertintotheleftsubtree
ifitem.value<node.item.value||(item.value==node.item.value&&item.seqNum<node.item.seqNum){
node.left=t.insert(node.left,item)
}else{
//否则,插入到右子树
//Otherwise,insertintotherightsubtree
node.right=t.insert(node.right,item)
}

//更新节点的高度
//Updatetheheightofthenode
node.height=1+max(height(node.left),height(node.right))

//平衡节点并返回平衡后的节点
//Balancethenodeandreturnthebalancednode
returnt.balance(node)
}

//deleteMin删除最小节点
//deleteMindeletestheminimumnode
func(t*SortingTree)deleteMin(node*Node)(*Node,Item){
//如果节点的左子节点为空,返回右子节点和当前节点的项
//Iftheleftchildofthenodeisnil,returntherightchildandthecurrentnode'sitem
ifnode.left==nil{
returnnode.right,node.item
}

//定义一个变量来存储最小项
//Defineavariabletostoretheminimumitem
varminItemItem

//递归调用deleteMin函数,删除左子树中的最小节点,并更新左子节点
//RecursivelycalldeleteMintodeletetheminimumnodeintheleftsubtreeandupdatetheleftchild
node.left,minItem=t.deleteMin(node.left)

//更新节点的高度
//Updatetheheightofthenode
node.height=1+max(height(node.left),height(node.right))

//平衡节点并返回平衡后的节点和最小项
//Balancethenodeandreturnthebalancednodeandtheminimumitem

1.4. 排序堆的性能测试

对使用了红黑树排序堆的 PushPopRemove 方法进行了性能测试。以下是排序堆的性能测试结果:

$gotest-benchmem-run=^$-bench^BenchmarkHeap*.
goos:darwin
goarch:amd64
pkg:github.com/shengyanli1982/workqueue/v2/internal/container/heap
cpu:Intel(R)Xeon(R)CPUE5-2643v2@3.50GHz
BenchmarkHeap_Push-125630415257.5ns/op0B/op0allocs/op
BenchmarkHeap_Pop-1216859534117.4ns/op0B/op0allocs/op
BenchmarkHeap_Remove-121484321728.197ns/op0B/op0allocs/op

2. 链表

实际上,这部分相当于对 WorkQueue 项目中的双向链表进行了重写,基本功能和性能与 container/list 包相似,只是增加了一些额外的功能,例如:「遍历」「交换」 等,以方便日后的使用。

2.1. 链表的实现

链表的实现与传统的链表实现没有太大区别,甚至可以说是一样的。

//List结构体代表一个双向链表,它有一个头节点和一个尾节点,以及一个记录链表长度的字段。
//TheListstructrepresentsadoublylinkedlist.Ithasaheadnode,atailnode,andafieldtorecordthelengthofthelist.
typeListstruct{
//head是链表的头节点。
//headistheheadnodeofthelist.
//tail是链表的尾节点。
//tailisthetailnodeofthelist.
head,tail*Node

//count是链表的长度。
//countisthelengthofthelist.
countint64
}

2.2. 链表的扩展

这个可能是最大的亮点。我在链表中增加了一些额外的功能,比如「遍历」「交换」等。这些功能对于依赖链表的 Queue 来说非常重要,例如,Queue 中的 Range 方法就依赖于链表的 Range 方法。

「Range 遍历链表」

//Range方法遍历链表,对每个节点执行fn函数,如果fn返回false,就停止遍历。
//TheRangemethodtraversesthelist,performsthefnfunctiononeachnode,andstopstraversingiffnreturnsfalse.
func(l*List)Range(fnfunc(node*Node)bool){
//我们从头节点开始,遍历整个链表。
//Westartfromtheheadnodeandtraversetheentirelist.
foriterNode:=l.head;iterNode!=nil;iterNode=iterNode.Right{
//我们对当前节点执行fn,如果fn返回false,我们就停止遍历。
//Weperformfnonthecurrentnode,iffnreturnsfalse,westoptraversing.
if!fn(iterNode){
break
}
}
}

「Swap 交换节点」

//Swap方法交换链表中的两个节点n和mark的位置。
//TheSwapmethodswapsthepositionsoftwonodesnandmarkinthelist.
func(l*List)Swap(node,mark*Node){
//如果n或mark是nil,或者n和mark是同一个节点,我们就直接返回,不做任何操作。
//Ifnormarkisnil,ornandmarkarethesamenode,wejustreturndirectlywithoutdoinganything.
ifnode==nil||mark==nil||node==mark{
return
}

//如果n或mark不是链表l的节点,我们就直接返回,不做任何操作。
//Ifnormarkisnotanodeoflistl,wejustreturndirectlywithoutdoinganything.
if!isPtrEqual(node.parentRef,l)||!isPtrEqual(mark.parentRef,l){
return
}

//如果n是mark的前一个节点,我们就移除n,然后将n插入到mark的后面。
//Ifnisthepreviousnodeofmark,weremovenandtheninsertnaftermark.
ifnode.Right==mark{
l.Remove(node)
l.InsertAfter(node,mark)
return
}

//如果n是mark的后一个节点,我们就移除n,然后将n插入到mark的前面。
//Ifnisthenextnodeofmark,weremovenandtheninsertnbeforemark.
ifnode.Left==mark{
l.Remove(node)
l.InsertBefore(node,mark)
return
}

//我们交换n和mark的Prev和Next。
//WeswapthePrevandNextofnandmark.
node.Left,mark.Left=mark.Left,node.Left
node.Right,mark.Right=mark.Right,node.Right

//如果n的Prev不是nil,我们就更新n的Prev的Next为n,否则,我们将链表的头节点设置为n。
//Ifn'sPrevisnotnil,weupdaten'sPrev'sNextton,otherwise,wesettheheadnodeofthelistton.
ifnode.Left!=nil{
node.Left.Right=node
}else{
l.head=node
}

//如果n的Next不是nil,我们就更新n的Next的Prev为n,否则,我们将链表的尾节点设置为n。
//Ifn'sNextisnotnil,weupdaten'sNext'sPrevton,otherwise,wesetthetailnodeofthelistton.
ifnode.Right!=nil{
node.Right.Left=node
}else{
l.tail=node
}

//如果mark的Prev不是nil,我们就更新mark的Prev的Next为mark,否则,我们将链表的头节点设置为mark。
//Ifmark'sPrevisnotnil,weupdatemark'sPrev'sNexttomark,otherwise,wesettheheadnodeofthelisttomark.
ifmark.Left!=nil{
mark.Left.Right=mark
}else{
l.head=mark
}

//如果mark的Next不是nil,我们就更新mark的Next的Prev为mark,否则,我们将链表的尾节点设置为mark。
//Ifmark'sNextisnotnil,weupdatemark'sNext'sPrevtomark,otherwise,wesetthetailnodeofthelisttomark.
ifmark.Right!=nil{
mark.Right.Left=mark
}else{
l.tail=mark
}
}

「Slice 链表元素切片」

//Slice方法将链表转换为一个切片,切片中的元素顺序和链表中的节点顺序一致。
//TheSlicemethodconvertsthelisttoaslice,theorderoftheelementsinthesliceisconsistentwiththeorderofthenodesinthelist.
func(l*List)Slice()[]interface{}{
//我们创建一个空的切片,切片的容量为链表的长度。
//Wecreateanemptyslice,thecapacityofthesliceisthelengthofthelist.
nodes:=make([]interface{},0,l.count)

//我们遍历链表,将每个节点的Value添加到切片中。
//WetraversethelistandaddtheValueofeachnodetotheslice.
l.Range(func(node*Node)bool{
nodes=append(nodes,node.Value)
returntrue
})

//返回切片。
//Returntheslice.
returnnodes
}

2.3. 链表性能测试

既然我们已经重写了链表,那么我们需要对链表进行性能测试,以评估其性能表现。链表的性能与 container/list 包的性能基本相同。

$gotest-benchmem-run=^$-bench^BenchmarkList*.
goos:darwin
goarch:amd64
pkg:github.com/shengyanli1982/workqueue/v2/internal/container/list
cpu:Intel(R)Xeon(R)CPUE5-2643v2@3.50GHz
BenchmarkList_PushBack-121869051076.447ns/op0B/op0allocs/op
BenchmarkList_PushFront-121573720527.701ns/op0B/op0allocs/op
BenchmarkList_PopBack-121795558466.645ns/op0B/op0allocs/op
BenchmarkList_PopFront-121800305826.989ns/op0B/op0allocs/op
BenchmarkList_InsertBefore-121892747716.406ns/op0B/op0allocs/op
BenchmarkList_InsertAfter-121600789816.490ns/op0B/op0allocs/op
BenchmarkList_Remove-121832507826.440ns/op0B/op0allocs/op
BenchmarkList_MoveToFront-121460212637.837ns/op0B/op0allocs/op
BenchmarkList_MoveToBack-121413364298.589ns/op0B/op0allocs/op
BenchmarkList_Swap-1210000000010.47ns/op0B/op0allocs/op

3. 接口设计

WorkQueuev2 版本重新设计了接口,使其更加简单、直观和易用。在接口设计上,我们遵循了 Go 语言的接口设计原则,如面向接口编程、接口隔离原则和依赖倒置原则等。这样做的好处是降低耦合度、提高代码的可维护性和可扩展性。

在接口设计方面,我们也遵循了 UML 类图优先代码的原则,即先绘制 UML 类图,再编写代码。

wqv2-7.png

通过 UML 类图,我们可以更好地理解 WorkQueuev2 版本的设计思路和实现原理,以及各个类之间的关系、属性和方法。

3.1. 接口设计原则

在设计接口时,我们遵循以下几个原则:

  1. 「面向接口编程」:通过定义接口来隔离具体实现,使得代码更加灵活和可扩展。
  2. 「接口隔离原则」:每个接口只包含客户端所需的方法,避免臃肿的接口。
  3. 「依赖倒置原则」:高层模块不应该依赖于低层模块,二者都应该依赖于抽象。

3.2. UML 类图解析

这份 .puml 文件是一个 PlantUML 图,描述了软件架构中各类和接口的结构及其关系。以下是文件内容的结构和关系的分析总结:

图表分为四个主要命名空间:config(配置),callback(回调),queue(队列) 和 limiter(限流器)。

3.2.1. 命名空间:config(配置)

    • QueueConfig(队列配置):方法包括 WithCallbackWithValueIdempotentWithSetCreator
    • DelayingQueueConfig(延迟队列配置):方法 WithCallback
    • PriorityQueueConfig(优先队列配置):方法 WithCallback
    • RateLimitingQueueConfig(限流队列配置):方法 WithCallbackWithLimiter
    • QueueConfigDelayingQueueConfigPriorityQueueConfig 的基类。
    • DelayingQueueConfigRateLimitingQueueConfig 的基类。

3.2.2. 命名空间:callback(回调)

    • QueueCallback(队列回调):方法包括 OnPutOnGetOnDone
    • DelayingQueueCallback(延迟队列回调):方法 OnDelayOnPullError
    • PriorityQueueCallback(优先队列回调):方法 OnPriority
    • RateLimitingQueueCallback(限流队列回调):方法 OnLimited
    • queueCallbackImpl(队列回调实现):实现了 QueueCallback
    • delayingQueueCallbackImpl(延迟队列回调实现):实现了 DelayingQueueCallback
    • priorityQueueCallbackImpl(优先队列回调实现):实现了 PriorityQueueCallback
    • ratelimitingQueueCallbackImpl(限流队列回调实现):实现了 RateLimitingQueueCallback

3.2.3. 命名空间:queue(队列)

    • Queue(队列):方法包括 ShutdownPutGetDone
    • delayingQueueImpl(延迟队列实现):方法包括 ShutdownPutWithDelayHeapRangeLen
    • priorityQueueImpl(优先队列实现):方法包括 ShutdownPutPutWithPriorityHeapRange
    • ratelimitingQueueImpl(限流队列实现):方法 ShutdownPutWithLimited
    • QueuedelayingQueueImplpriorityQueueImplratelimitingQueueImpl 的基类。
    • DelayingQueueratelimitingQueueImpl 的基类。
    • PriorityQueuepriorityQueueImpl 的基类。

3.2.4. 命名空间:limiter(限流器)

    • bucketRateLimiterImpl(桶限流器实现):实现了 Limiter,方法 When
    • nopRateLimiterImpl(空限流器实现):实现了 Limiter,方法 When。## 4. 功能设计

在本节中,我们将详细介绍项目的主要功能模块及其设计思路。主要包括优先级队列和延迟队列的设计与实现。

4. 功能设计

本节将详细介绍项目的主要功能模块及其设计思路,主要包括优先级队列和延迟队列的设计与实现。

4.1 Priority Queue 优先级队列

优先级队列是一种特殊的队列数据结构,其中每个元素都有一个优先级,优先级高的元素会优先被处理。在设计优先级队列时,我们需要考虑以下几点:

  1. 「数据结构选择」:选择使用堆(Heap)来实现优先级队列,因为堆能够高效地进行插入和删除操作。
  2. 「接口设计」:优先级队列需要提供插入、删除、获取最高优先级元素等基本操作接口。
  3. 「性能优化」:通过优化堆的实现,确保优先级队列在高并发场景下的性能表现。
  4. 「测试与验证」:编写单元测试和性能测试,确保优先级队列的正确性和高效性。

WorkQueue「v1」 版本中,我在 README.md 中这样描述:

wqv2-8.png

在优先级队列中,需要一个时间窗口来对当前添加到队列中的元素进行排序。在这个时间窗口内的元素按照优先级升序排序。然而,即使两个时间窗口紧邻,两个窗口中的元素的顺序也不能保证按照优先级排序。

这个 排序窗口 的概念让很多开发者感到困惑,因为在传统的优先级队列中并没有这样的概念。此外,开发者还需要考虑 排序窗口 的大小,根据窗口大小来决定是否需要对队列中的元素进行排序。

这样一来,导致了 PriorityQueue 的实现变得复杂,并且与传统的优先级队列有很大的不同。在 「v2」 版本中,我对 PriorityQueue 进行了重新设计,去掉了 排序窗口 的概念,遵循通用的 PriorityQueue 概念,使得 PriorityQueue 的实现更加简单、直观和易用。

使用通用的 PriorityQueue 概念,可能会出现一个问题:如果存在一个十分低优先级的任务,那么这个任务就会一直被堵塞在队列中,无法被处理。这个问题在 「v1」 版本中是不存在的,因为 排序窗口 的存在可以保证低优先级的任务也能被及时处理。然而,在 「v2」 版本中,这个问题是存在的,开发者需要注意这一点。

WorkQueue「v2」 版本中,我在 README.md 中这样描述:

wqv2-9.png

因此,在 「v2」 版本中,PriorityQueue 回归了传统的优先级队列的概念,使得开发者在使用 PriorityQueue 时更符合直觉,不会因为 排序窗口 的存在而感到困惑。

4.2 Delaying Queue 延迟队列

延迟队列是一种特殊的队列数据结构,其中每个元素都有一个延迟时间,只有当延迟时间到达后,元素才会被处理。在设计延迟队列时,我们主要考虑以下几点:

  1. 「数据结构选择」:选择使用时间堆(Time Heap)来实现延迟队列,因为时间堆能够高效地管理定时任务。
  2. 「接口设计」:延迟队列需要提供插入、删除、获取到期元素等基本操作接口。
  3. 「时间管理」:通过精确的时间管理,确保延迟队列能够准确地处理到期元素。
  4. 「测试与验证」:编写单元测试和性能测试,确保延迟队列的正确性和高效性。

DelayingQueue 是日常使用和项目开发中最常见的一种队列,它可以用来处理定时任务、延迟任务等。然而,处理延迟任务的过期问题一直困扰着我。

在设计过程中,我考虑了两种方案:使用 time.Timer 和使用 time.Ticker 来处理延迟任务的过期问题。经过深思熟虑和讨论,我最终选择了使用 time.Ticker

首先,time.Timer 是一个单次触发的定时器,只能触发一次,而 time.Ticker 是一个周期触发的定时器,可以周期性地触发。在延迟队列中,我们需要周期性地检查延迟任务是否过期,因此 time.Ticker 更适合处理延迟任务的过期问题。

虽然 time.Ticker 的周期执行间隔是固定的,无法动态调整,但在延迟队列中,我们可以根据队列中的元素动态调整执行间隔。这是 time.Timer 支持者的一个观点,因为 time.Timer 可以通过 Reset 方法动态调整执行间隔。

然而,我选择使用 time.Ticker 的原因是如何处理延迟任务的到期和数据处理的问题。

  • 如果逐一处理延迟任务,那么 time.Timer 是最好的选择。每次处理完一个任务后,根据下一个任务的延迟时间调整 time.Timer 的执行时间。
  • 如果批量处理延迟任务,那么 time.Ticker 是最好的选择。每次 time.Ticker 触发时,我们处理所有到期的任务。

在大量延迟任务同时到期的情况下,逐一处理延迟任务并反复调整 time.Timer 的执行时间会导致性能下降。而批量处理延迟任务,一批任务一起处理,可以减少调整 time.Timer 的次数,提高性能。这里借鉴了 「v1」 版本中窗口的概念,将延迟任务分为多个窗口,每个窗口的大小是固定的,当窗口中的任务到期时,处理该窗口中的所有任务。

我们通过一组图表来展示 DelayingQueue 中的窗口概念:

wqv2-10.png

经过广泛和深入的讨论,我们最终确定了窗口间隔的时间为 「300ms」,这个值经过多次测试和调整得出。这个值既能保证延迟任务的及时处理,又能保证性能的高效性.

WorkQueue「v2」 版本中,我在 README.md 中这样描述:

wqv2-11.png

使用批量任务的窗口方法虽然可以满足各种高性能的需求,但是也不是无限可击的,因为窗口的大小是固定的,如果窗口中的任务过分的稀疏,那么可能会导致延迟任务处理没有及时性。我在 README.md 也提到了这一点:「“This means that there may be a slight deviation in the actual delay time of the element. The actual mini delay time is the “element delay time + 300ms”.”」

5. 性能测试

我对 WorkQueuev2 版本进行了一系列性能测试,主要测试了 QueueDelayingQueuePriorityQueueRateLimitingQueue 四个队列的性能。以下是性能测试的结果:

$gotest-benchmem-run=^$-bench^Benchmark*.
goos:darwin
goarch:amd64
pkg:github.com/shengyanli1982/workqueue/v2
cpu:Intel(R)Xeon(R)CPUE5-2643v2@3.50GHz
BenchmarkDelayingQueue_Put-124635976255.6ns/op72B/op1allocs/op
BenchmarkDelayingQueue_PutWithDelay-121635588784.7ns/op71B/op1allocs/op
BenchmarkDelayingQueue_Get-122479513647.53ns/op21B/op0allocs/op
BenchmarkDelayingQueue_PutAndGet-121599589075.25ns/op7B/op0allocs/op
BenchmarkDelayingQueue_PutWithDelayAndGet-121731825664.3ns/op29B/op1allocs/op
BenchmarkPriorityQueue_Put-123030818433.5ns/op71B/op1allocs/op
BenchmarkPriorityQueue_PutWithPriority-122937105452.0ns/op71B/op1allocs/op
BenchmarkPriorityQueue_Get-1211245106134.3ns/op23B/op0allocs/op
BenchmarkPriorityQueue_PutAndGet-121296203192.24ns/op7B/op0allocs/op
BenchmarkPriorityQueue_PutWithPriorityAndGet-121454376983.70ns/op7B/op0allocs/op
BenchmarkQueue_Put-126102608206.1ns/op71B/op1allocs/op
BenchmarkQueue_Get-123030467545.30ns/op17B/op0allocs/op
BenchmarkQueue_PutAndGet-121717117471.83ns/op7B/op0allocs/op
BenchmarkQueue_Idempotent_Put-121573570706.9ns/op136B/op3allocs/op
BenchmarkQueue_Idempotent_Get-122275533534.4ns/op105B/op0allocs/op
BenchmarkQueue_Idempotent_PutAndGet-122551188494.5ns/op75B/op1allocs/op
BenchmarkRateLimitingQueue_Put-125852602214.0ns/op71B/op1allocs/op
BenchmarkRateLimitingQueue_PutWithLimited-121412991852.6ns/op135B/op2allocs/op
BenchmarkRateLimitingQueue_Get-122818606349.60ns/op19B/op0allocs/op
BenchmarkRateLimitingQueue_PutAndGet-121560067975.69ns/op7B/op0allocs/op
BenchmarkRateLimitingQueue_PutWithLimitedAndGet-121395084855.5ns/op135B/op2allocs/op

项目介绍

WorkQueue https://github.com/shengyanli1982/workqueue

wqv2-6.png

WorkQueue 是一个基于 Go 语言实现的高性能、高可靠、高可扩展的工作队列库,它可以帮助你快速构建一个高性能的工作队列系统,用于处理大量的工作任务。WorkQueue 的 v2 版本是在 v1 版本的基础上进行了全面的重构和优化,主要包括以下几个方面的改进:

  • 「排序堆」WorkQueue 的 v2 版本使用了红黑树作为排序堆的实现,保证了堆的稳定性和性能。
  • 「接口设计」WorkQueue 的 v2 版本重新设计了接口,使得接口更加简单、直观和易用。
  • 「功能设计」WorkQueue 的 v2 版本重新设计了功能,使得功能更加完善、稳定和高效。
  • 「代码质量」WorkQueue 的 v2 版本重新设计了代码,使得代码更加简洁、清晰和高质量。

WorkQueue 提供了以下几个主要的队列类型:

  1. Queue: 一个基本的工作队列,是项目的基础,也是其他队列的基类。它提供了一个队列的完整功能,包括:入队、出队、遍历、清空等等。
  2. DelayingQueue: 一个延迟队列,是在 Queue 的基础上增加了延迟的功能。
  3. PriorityQueue: 一个优先级队列,是在 Queue 的基础上增加了优先级的功能。
  4. RateLimitingQueue: 一个限速队列,是在 DelayingQueue 的基础上增加了限速的功能。

快速上手

以下是使用 WorkQueue 的快速入门指南。只需按照以下四个步骤进行操作即可:

  1. 「导入包」

    首先,我们需要导入 WorkQueue 包。这个包提供了创建和管理工作队列的所有必要功能。

    importwkq"github.com/shengyanli1982/workqueue/v2"
  2. 「创建队列」

    接下来,我们需要创建一个新的队列实例。NewQueue 函数可以接受一个可选的配置参数,这里我们传入 nil 使用默认配置。

    queue:=wkq.NewQueue(nil)
  3. 「使用队列」

    现在,我们可以开始使用队列了。首先,我们将一个元素(例如字符串 “hello”)放入队列中。然后,我们可以从队列中获取这个元素。Put 方法用于将元素放入队列,而 Get 方法用于从队列中取出元素。

    //将"hello"放入队列
    _=queue.Put("hello")

    //从队列中获取元素
    element,_:=queue.Get()

    在实际应用中,您可以将任何类型的元素放入队列,并根据需要从队列中获取和处理这些元素。

  4. 「关闭队列」

    最后,当我们不再需要使用队列时,可以调用 Shutdown 方法关闭队列。这将停止队列的所有操作,并释放相关资源。

    queue.Shutdown()

通过以上步骤,您已经成功创建并使用了一个 WorkQueue 实例。这个简单的示例展示了如何快速上手使用 WorkQueue,在实际项目中,您可以根据需要进一步扩展和定制队列的功能。

完整示例

以下是一个完整的示例,展示了如何使用 WorkQueue 包来创建和管理工作队列:

「演示代码」

packagemain

import(
"errors"
"fmt"
"sync"
"time"

wkq"github.com/shengyanli1982/workqueue/v2"
)

//consumer函数是一个消费者函数,它从队列中获取元素并处理它们
//Theconsumerfunctionisaconsumerfunctionthatgetselementsfromthequeueandprocessesthem
funcconsumer(queuewkq.Queue,wg*sync.WaitGroup){
//当函数返回时,调用wg.Done()来通知WaitGroup一个任务已经完成
//Whenthefunctionreturns,callwg.Done()tonotifytheWaitGroupthatataskhasbeencompleted
deferwg.Done()

//无限循环,直到函数返回
//Infiniteloopuntilthefunctionreturns
for{
//从队列中获取一个元素
//Getanelementfromthequeue
element,err:=queue.Get()

//如果获取元素时发生错误,则处理错误
//Ifanerroroccurswhengettingtheelement,handletheerror
iferr!=nil{
//如果错误不是因为队列为空,则打印错误并返回
//Iftheerrorisnotbecausethequeueisempty,printtheerrorandreturn
if!errors.Is(err,wkq.ErrQueueIsEmpty){
fmt.Println(err)
return
}else{
//如果错误是因为队列为空,则继续循环
//Iftheerrorisbecausethequeueisempty,continuetheloop
continue
}
}

//打印获取到的元素
//Printtheobtainedelement
fmt.Println(">getelement:",element)

//标记元素为已处理,'Done'是在'Get'之后必需的
//Marktheelementasdone,'Done'isrequiredafter'Get'
queue.Done(element)
}
}

funcmain(){
//创建一个WaitGroup,用于等待所有的goroutine完成
//CreateaWaitGrouptowaitforallgoroutinestocomplete
wg:=sync.WaitGroup{}

//创建一个新的队列
//Createanewqueue
queue:=wkq.NewQueue(nil)

//增加WaitGroup的计数器
//IncreasethecounteroftheWaitGroup
wg.Add(1)

//启动一个新的goroutine来运行comsumer函数
//Startanewgoroutinetorunthecomsumerfunction
goconsumer(queue,&wg)

//将"hello"放入队列
//Put"hello"intothequeue
_=queue.Put("hello")

//将"world"放入队列
//Put"world"intothequeue
_=queue.Put("world")

//等待一秒钟,让comsumer有机会处理队列中的元素
//Waitforasecondtogivethecomsumerachancetoprocesstheelementsinthequeue
time.Sleep(time.Second)

//关闭队列
//Shutdownthequeue
queue.Shutdown()

//等待所有的goroutine完成
//Waitforallgoroutinestocomplete
wg.Wait()
}

「运行结果」

$gorundemo.go
>getelement:hello
>getelement:world
queueisshuttingdown

总结

在这次的经历中,我重新审视了许多基本概念,例如:「排序堆」「链表」「接口设计」「功能设计」「代码质量」 等等。在这个过程中,我积累了大量宝贵的经验,包括:「如何选择合适的排序堆」「如何设计高效的链表」「如何进行接口设计」「如何设计功能模块」「如何提升代码质量」 等等。这些经验对我未来的工作和学习都将大有裨益。

经过深入的思考和总结,我认为一个优秀的项目应该具备以下几个特点:

  • 「稳定」:保证系统的稳定性,能够在各种情况下正常运行。
  • 「易用」:提供良好的用户体验,使用户能够轻松上手。
  • 「可扩展」:具备良好的扩展性,能够适应未来的需求变化。

经过一段时间的迭代和优化,WorkQueuev2 版本已经基本实现了上述特点。我们进行了广泛的测试,并在内部生产系统和外部用户的实际应用中得到了验证,证明了其稳定性和可靠性。

我写这篇文章的目的是分享我在开发 WorkQueue 过程中的经验和心得,希望能够帮助到其他开发者。同时,我也希望能够与大家一起完善这个项目。如果你对这个项目感兴趣,欢迎访问我们的仓库,并提出你的建议和意见。我会尽快进行跟进,与大家共同努力,使 WorkQueue 变得更加完善和强大。

点击公众关注号,“技术干货”及时达!