从 v1 到 v2:分享我 WorkQueue 项目重构的完整思路

8,551 阅读42分钟

我是 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. 保证堆序的稳定性,不受堆大小和传入数据变化的影响。
  3. 提供高效的堆排序算法,不受堆中元素或插入顺序变化的影响。

预期验证代码:

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

func HeapPush_Random() {
	h := New()
	count := 4

	// Push the nodes
	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)})

	// Prrint the tree order
	PrintRootIndexs(h)
	PrintOrderTraversalIndexs(h.root)
}

2. 接口设计

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

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

预期验证代码:

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

PriorityQueue 结构体

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

// PriorityQueue 接口继承了 Queue 接口,并添加了一个 PutWithPriority 方法,用于将元素按优先级放入队列。
// The PriorityQueue interface inherits from the Queue interface and adds a PutWithPriority method to put an element into the queue with priority.
type PriorityQueue = interface {
	Queue

	// PutWithPriority 方法用于将元素按优先级放入队列。
	// The PutWithPriority method is used to put an element into the queue with priority.
	PutWithPriority(value interface{}, priority int64) error

	// HeapRange 方法用于遍历 sorted 堆中的所有元素。
	// The HeapRange method is used to traverse all elements in the sorted heap.
	HeapRange(fn func(value interface{}, delay int64) bool)
}

// priorityQueueImpl 结构体,实现了 PriorityQueue 接口
// The priorityQueueImpl struct, which implements the PriorityQueue interface
type priorityQueueImpl struct {
	// Queue 是一个队列接口
	// Queue is a queue interface
	Queue

	...
}

PriorityQueue 配置参数

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

// PriorityQueueConfig 结构体,用于配置优先队列
// The PriorityQueueConfig struct, used for configuring the priority queue
type PriorityQueueConfig struct {
	// QueueConfig 是队列的配置
	// QueueConfig is the configuration of the queue
	QueueConfig

	// callback 是优先队列的回调函数
	// callback is the callback function of the priority queue
	callback PriorityQueueCallback
}

PriorityQueue 回调函数

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

// PriorityQueueCallback 接口继承了 QueueCallback 接口,并添加了 OnPriority 方法。
// The PriorityQueueCallback interface inherits from the QueueCallback interface and adds the OnPriority method.
type PriorityQueueCallback = interface {
	QueueCallback

	// OnPriority 方法在元素被按优先级放入队列时被调用。
	// The OnPriority method is called when an element is put into the queue with priority.
	OnPriority(value interface{}, priority int64)
}

PriorityQueue 创建方法

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

// NewPriorityQueue 函数用于创建一个新的 PriorityQueue
// The NewPriorityQueue function is used to create a new PriorityQueue
func NewPriorityQueue(config *PriorityQueueConfig) PriorityQueue {
	...
}

3. 功能问题

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

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

4. 代码质量

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

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

5. 架构设计

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

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

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

思考过程

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

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

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

重构和优化

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

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

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 方法用于向红黑树中插入一个节点
    // The insert method is used to insert a node into the RED-BLACK tree
    func (tree *RBTree) insert(node *lst.Node) {
    
        // parent 用于保存当前节点的父节点
        // parent is used to save the parent of the current node
        var parent *lst.Node
    
        // current 用于保存当前正在处理的节点,初始为树根
        // current is used to save the node currently being processed, initially the root of the tree
        current := tree.root
    
        // 当 current 不为空时,进行循环
        // Loop when current is not nil
        for current != nil {
    
            // 将 parent 设置为 current
            // Set parent to current
            parent = current
    
            // 如果 node 的优先级小于 current 的优先级
            // If the priority of node is less than the priority of current
            if node.Priority < current.Priority {
    
                // 将 current 设置为其左子节点
                // Set current to its left child
                current = current.Left
    
            } else {
    
                // 将 current 设置为其右子节点
                // Set current to its right child
                current = current.Right
    
            }
    
        }
    
        // 将 node 的父节点设置为 parent
        // Set the parent of node to parent
        node.Parent = parent
    
        // 如果 parent 为空
        // If parent is nil
        if parent == nil {
    
            // 将树根设置为 node
            // Set the root of the tree to node
            tree.root = node
    
        } else if node.Priority < parent.Priority { // 如果 node 的优先级小于 parent 的优先级
    
            // 将 parent 的左子节点设置为 node
            // Set the left child of parent to node
            parent.Left = node
    
        } else {
    
            // 将 parent 的右子节点设置为 node
            // Set the right child of parent to node
            parent.Right = node
    
        }
    
        // 将 node 的左子节点和右子节点都设置为 nil
        // Set both the left child and right child of node to nil
        node.Left = nil
        node.Right = nil
    
        // 将 node 的颜色设置为红色
        // Set the color of node to RED
        node.Color = lst.RED
    
        // 调用 insertFixUp 方法对树进行调整
        // Call the insertFixUp method to adjust the tree
        insertFixUp(tree, node)
    
        // 将树的节点数量加一
        // Increase the number of nodes in the tree by one
        tree.count++
    
        // 如果树的头节点为空或 node 的优先级小于头节点的优先级
        // If the head of the tree is nil or the priority of node is less than the priority of the head
        if tree.head == nil || node.Priority < tree.head.Priority {
    
            // 将树的头节点设置为 node
            // Set the head of the tree to node
            tree.head = node
    
        }
    
        // 如果树的尾节点为空或 node 的优先级大于尾节点的优先级
        // If the tail of the tree is nil or the priority of node is greater than the priority of the tail
        if tree.tail == nil || node.Priority > tree.tail.Priority {
    
            // 将树的尾节点设置为 node
            // Set the tail of the tree to node
            tree.tail = node
    
        }
    
    }
    
  • 删除节点

    // delete 函数用于删除红黑树中的节点
    // The delete function is used to delete a node in the RED-BLACK tree
    func (tree *RBTree) delete(node *lst.Node) {
    
        // 定义 child 和 target 节点
        // Define child and target nodes
        var child, target *lst.Node
    
        // 如果 node 的左子节点为空或右子节点为空
        // If the left child of node is nil or the right child is nil
        if node.Left == nil || node.Right == nil {
    
            // target 设置为 node
            // Set target to node
            target = node
    
        } else {
    
            // target 设置为 node 的后继节点
            // Set target to the successor of node
            target = tree.successor(node)
    
        }
    
        // 如果 target 的左子节点不为空
        // If the left child of target is not nil
        if target.Left != nil {
    
            // child 设置为 target 的左子节点
            // Set child to the left child of target
            child = target.Left
    
        } else {
    
            // child 设置为 target 的右子节点
            // Set child to the right child of target
            child = target.Right
    
        }
    
        // 如果 child 不为空
        // If child is not nil
        if child != nil {
    
            // 将 child 的父节点设置为 target 的父节点
            // Set the parent of child to the parent of target
            child.Parent = target.Parent
    
        }
    
        // 如果 target 的父节点为空
        // If the parent of target is nil
        if target.Parent == nil {
    
            // 将树根设置为 child
            // Set the root of the tree to child
            tree.root = child
    
        } else if target == target.Parent.Left { // 如果 target 是其父节点的左子节点
    
            // 将 target 的父节点的左子节点设置为 child
            // Set the left child of the parent of target to child
            target.Parent.Left = child
    
        } else { // 如果 target 是其父节点的右子节点
    
            // 将 target 的父节点的右子节点设置为 child
            // Set the right child of the parent of target to child
            target.Parent.Right = child
    
        }
    
        // 如果 target 不等于 node
        // If target is not equal to node
        if target != node {
    
            // 将 node 的值设置为 target 的值
            // Set the value of node to the value of target
            node.Value = target.Value
    
            // 将 node 的优先级设置为 target 的优先级
            // Set the priority of node to the priority of target
            node.Priority = target.Priority
    
        }
    
        // 如果 target 的颜色为黑色
        // If the color of target is BLACK
        if target.Color == lst.BLACK {
    
            // 调用 deleteFixUp 函数调整红黑树
            // Call the deleteFixUp function to adjust the RED-BLACK tree
            deleteFixUp(tree, child)
    
        }
    
        // 将树的节点数量减一
        // Decrease the number of nodes in the tree by one
        tree.count--
    
        // 如果树的节点数量为零
        // If the number of nodes in the tree is zero
        if tree.count == 0 {
    
            // 将树的头节点和尾节点设置为 nil
            // Set the head and tail nodes of the tree to nil
            tree.head = nil
            tree.tail = nil
    
        } else {
    
            // 如果树的头节点等于 node
            // If the head node of the tree is equal to node
            if tree.head == node {
    
                // 将树的头节点设置为树根的最小节点
                // Set the head node of the tree to the minimum node of the root
                tree.head = tree.minimum(tree.root)
    
            }
    
            // 如果树的尾节点等于 node
            // If the tail node of the tree is equal to node
            if tree.tail == node {
    
                // 将树的尾节点设置为树根的最大节点
                // Set the tail node of the tree to the maximum node of the root
                tree.tail = tree.maximum(tree.root)
    
            }
    
        }
    
    }
    

1.3. 排序堆的优化经历

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

插入排序

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

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

示意图

wqv2-3.png

演示代码

// Push 方法将一个新的节点添加到堆中。
// The Push method adds a new node to the heap.
func (h *Heap) Push(n *lst.Node) {
	// 如果 n 是 nil 或者堆的长度为 0,我们就直接在链表的后面添加 n。
	// If n is nil or the length of the heap is 0, we just add n to the back of the list.
	if n == nil || h.list.Len() == 0 {
		h.list.PushBack(n)
		return
	}

	// 获取链表的最后一个节点。
	// Get the last node of the list.
	current := h.list.Back()

	// 如果当前节点不是 nil 并且 n 小于当前节点,我们就继续向前查找。
	// If the current node is not nil and n is less than the current node, we continue to look forward.
	for current != nil && h.less(n, current) {
		current = current.Prev
	}

	// 如果当前节点是 nil,我们就在链表的前面添加 n。
	// If the current node is nil, we add n to the front of the list.
	if current == nil {
		h.list.PushFront(n)
	} else {
		// 否则,我们就在当前节点的后面添加 n。
		// Otherwise, we add n after the current node.
		h.list.InsertAfter(n, current)
	}
}
AVL 树

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

示意图

wqv2-4.png wqv2-5.png

演示代码

// insert 插入新节点
// insert inserts a new node
func (t *SortingTree) insert(node *Node, item Item) *Node {
    // 如果节点为空,创建一个新节点并返回
    // If the node is nil, create a new node and return it
    if node == nil {
        return &Node{item: item, height: 1}
    }

    // 如果插入项的值小于当前节点的值,或者值相等但序列号小于当前节点的序列号,则插入到左子树
    // If the item's value is less than the current node's value, or the values are equal but the sequence number is less, insert into the left subtree
    if item.value < node.item.value || (item.value == node.item.value && item.seqNum < node.item.seqNum) {
        node.left = t.insert(node.left, item)
    } else {
        // 否则,插入到右子树
        // Otherwise, insert into the right subtree
        node.right = t.insert(node.right, item)
    }

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

    // 平衡节点并返回平衡后的节点
    // Balance the node and return the balanced node
    return t.balance(node)
}

// deleteMin 删除最小节点
// deleteMin deletes the minimum node
func (t *SortingTree) deleteMin(node *Node) (*Node, Item) {
    // 如果节点的左子节点为空,返回右子节点和当前节点的项
    // If the left child of the node is nil, return the right child and the current node's item
    if node.left == nil {
        return node.right, node.item
    }

    // 定义一个变量来存储最小项
    // Define a variable to store the minimum item
    var minItem Item

    // 递归调用 deleteMin 函数,删除左子树中的最小节点,并更新左子节点
    // Recursively call deleteMin to delete the minimum node in the left subtree and update the left child
    node.left, minItem = t.deleteMin(node.left)

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

    // 平衡节点并返回平衡后的节点和最小项
    // Balance the node and return the balanced node and the minimum item

1.4. 排序堆的性能测试

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

$ go test -benchmem -run=^$ -bench ^BenchmarkHeap* .
goos: darwin
goarch: amd64
pkg: github.com/shengyanli1982/workqueue/v2/internal/container/heap
cpu: Intel(R) Xeon(R) CPU E5-2643 v2 @ 3.50GHz
BenchmarkHeap_Push-12       5630415         257.5 ns/op         0 B/op         0 allocs/op
BenchmarkHeap_Pop-12        16859534        117.4 ns/op         0 B/op         0 allocs/op
BenchmarkHeap_Remove-12     148432172       8.197 ns/op         0 B/op         0 allocs/op

2. 链表

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

2.1. 链表的实现

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

// List 结构体代表一个双向链表,它有一个头节点和一个尾节点,以及一个记录链表长度的字段。
// The List struct represents a doubly linked list. It has a head node, a tail node, and a field to record the length of the list.
type List struct {
	// head 是链表的头节点。
	// head is the head node of the list.
	// tail 是链表的尾节点。
	// tail is the tail node of the list.
	head, tail *Node

	// count 是链表的长度。
	// count is the length of the list.
	count int64
}

2.2. 链表的扩展

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

Range 遍历链表

// Range 方法遍历链表,对每个节点执行 fn 函数,如果 fn 返回 false,就停止遍历。
// The Range method traverses the list, performs the fn function on each node, and stops traversing if fn returns false.
func (l *List) Range(fn func(node *Node) bool) {
	// 我们从头节点开始,遍历整个链表。
	// We start from the head node and traverse the entire list.
	for iterNode := l.head; iterNode != nil; iterNode = iterNode.Right {
		// 我们对当前节点执行 fn,如果 fn 返回 false,我们就停止遍历。
		// We perform fn on the current node, if fn returns false, we stop traversing.
		if !fn(iterNode) {
			break
		}
	}
}

Swap 交换节点

// Swap 方法交换链表中的两个节点 n 和 mark 的位置。
// The Swap method swaps the positions of two nodes n and mark in the list.
func (l *List) Swap(node, mark *Node) {
	// 如果 n 或 mark 是 nil,或者 n 和 mark 是同一个节点,我们就直接返回,不做任何操作。
	// If n or mark is nil, or n and mark are the same node, we just return directly without doing anything.
	if node == nil || mark == nil || node == mark {
		return
	}

	// 如果 n 或 mark 不是链表 l 的节点,我们就直接返回,不做任何操作。
	// If n or mark is not a node of list l, we just return directly without doing anything.
	if !isPtrEqual(node.parentRef, l) || !isPtrEqual(mark.parentRef, l) {
		return
	}

	// 如果 n 是 mark 的前一个节点,我们就移除 n,然后将 n 插入到 mark 的后面。
	// If n is the previous node of mark, we remove n and then insert n after mark.
	if node.Right == mark {
		l.Remove(node)
		l.InsertAfter(node, mark)
		return
	}

	// 如果 n 是 mark 的后一个节点,我们就移除 n,然后将 n 插入到 mark 的前面。
	// If n is the next node of mark, we remove n and then insert n before mark.
	if node.Left == mark {
		l.Remove(node)
		l.InsertBefore(node, mark)
		return
	}

	// 我们交换 n 和 mark 的 Prev 和 Next。
	// We swap the Prev and Next of n and mark.
	node.Left, mark.Left = mark.Left, node.Left
	node.Right, mark.Right = mark.Right, node.Right

	// 如果 n 的 Prev 不是 nil,我们就更新 n 的 Prev 的 Next 为 n,否则,我们将链表的头节点设置为 n。
	// If n's Prev is not nil, we update n's Prev's Next to n, otherwise, we set the head node of the list to n.
	if node.Left != nil {
		node.Left.Right = node
	} else {
		l.head = node
	}

	// 如果 n 的 Next 不是 nil,我们就更新 n 的 Next 的 Prev 为 n,否则,我们将链表的尾节点设置为 n。
	// If n's Next is not nil, we update n's Next's Prev to n, otherwise, we set the tail node of the list to n.
	if node.Right != nil {
		node.Right.Left = node
	} else {
		l.tail = node
	}

	// 如果 mark 的 Prev 不是 nil,我们就更新 mark 的 Prev 的 Next 为 mark,否则,我们将链表的头节点设置为 mark。
	// If mark's Prev is not nil, we update mark's Prev's Next to mark, otherwise, we set the head node of the list to mark.
	if mark.Left != nil {
		mark.Left.Right = mark
	} else {
		l.head = mark
	}

	// 如果 mark 的 Next 不是 nil,我们就更新 mark 的 Next 的 Prev 为 mark,否则,我们将链表的尾节点设置为 mark。
	// If mark's Next is not nil, we update mark's Next's Prev to mark, otherwise, we set the tail node of the list to mark.
	if mark.Right != nil {
		mark.Right.Left = mark
	} else {
		l.tail = mark
	}
}

Slice 链表元素切片

// Slice 方法将链表转换为一个切片,切片中的元素顺序和链表中的节点顺序一致。
// The Slice method converts the list to a slice, the order of the elements in the slice is consistent with the order of the nodes in the list.
func (l *List) Slice() []interface{} {
	// 我们创建一个空的切片,切片的容量为链表的长度。
	// We create an empty slice, the capacity of the slice is the length of the list.
	nodes := make([]interface{}, 0, l.count)

	// 我们遍历链表,将每个节点的 Value 添加到切片中。
	// We traverse the list and add the Value of each node to the slice.
	l.Range(func(node *Node) bool {
		nodes = append(nodes, node.Value)
		return true
	})

	// 返回切片。
	// Return the slice.
	return nodes
}

2.3. 链表性能测试

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

$ go test -benchmem -run=^$ -bench ^BenchmarkList* .
goos: darwin
goarch: amd64
pkg: github.com/shengyanli1982/workqueue/v2/internal/container/list
cpu: Intel(R) Xeon(R) CPU E5-2643 v2 @ 3.50GHz
BenchmarkList_PushBack-12        	186905107	         6.447 ns/op	       0 B/op	       0 allocs/op
BenchmarkList_PushFront-12       	157372052	         7.701 ns/op	       0 B/op	       0 allocs/op
BenchmarkList_PopBack-12         	179555846	         6.645 ns/op	       0 B/op	       0 allocs/op
BenchmarkList_PopFront-12        	180030582	         6.989 ns/op	       0 B/op	       0 allocs/op
BenchmarkList_InsertBefore-12    	189274771	         6.406 ns/op	       0 B/op	       0 allocs/op
BenchmarkList_InsertAfter-12     	160078981	         6.490 ns/op	       0 B/op	       0 allocs/op
BenchmarkList_Remove-12          	183250782	         6.440 ns/op	       0 B/op	       0 allocs/op
BenchmarkList_MoveToFront-12     	146021263	         7.837 ns/op	       0 B/op	       0 allocs/op
BenchmarkList_MoveToBack-12      	141336429	         8.589 ns/op	       0 B/op	       0 allocs/op
BenchmarkList_Swap-12            	100000000	         10.47 ns/op	       0 B/op	       0 allocs/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
  1. 关系

    • QueueConfigDelayingQueueConfigPriorityQueueConfig 的基类。
    • DelayingQueueConfigRateLimitingQueueConfig 的基类。
3.2.2. 命名空间:callback(回调)
  1. 接口

    • 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
  1. 关系

    • QueuedelayingQueueImplpriorityQueueImplratelimitingQueueImpl 的基类。
    • DelayingQueueratelimitingQueueImpl 的基类。
    • PriorityQueuepriorityQueueImpl 的基类。
3.2.4. 命名空间:limiter(限流器)
  1. 接口

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

4. 功能设计

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

4.1 Priority Queue 优先级队列

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

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

WorkQueuev1 版本中,我在 README.md 中这样描述:

wqv2-8.png

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

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

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

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

WorkQueuev2 版本中,我在 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,这个值经过多次测试和调整得出。这个值既能保证延迟任务的及时处理,又能保证性能的高效性.

WorkQueuev2 版本中,我在 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 四个队列的性能。以下是性能测试的结果:

$ go test -benchmem -run=^$ -bench ^Benchmark* .
goos: darwin
goarch: amd64
pkg: github.com/shengyanli1982/workqueue/v2
cpu: Intel(R) Xeon(R) CPU E5-2643 v2 @ 3.50GHz
BenchmarkDelayingQueue_Put-12                             4635976           255.6 ns/op        72 B/op          1 allocs/op
BenchmarkDelayingQueue_PutWithDelay-12                    1635588           784.7 ns/op        71 B/op          1 allocs/op
BenchmarkDelayingQueue_Get-12                            24795136            47.53 ns/op       21 B/op          0 allocs/op
BenchmarkDelayingQueue_PutAndGet-12                      15995890            75.25 ns/op        7 B/op          0 allocs/op
BenchmarkDelayingQueue_PutWithDelayAndGet-12              1731825           664.3 ns/op        29 B/op          1 allocs/op
BenchmarkPriorityQueue_Put-12                             3030818           433.5 ns/op        71 B/op          1 allocs/op
BenchmarkPriorityQueue_PutWithPriority-12                 2937105           452.0 ns/op        71 B/op          1 allocs/op
BenchmarkPriorityQueue_Get-12                            11245106           134.3 ns/op        23 B/op          0 allocs/op
BenchmarkPriorityQueue_PutAndGet-12                      12962031            92.24 ns/op        7 B/op          0 allocs/op
BenchmarkPriorityQueue_PutWithPriorityAndGet-12          14543769            83.70 ns/op        7 B/op          0 allocs/op
BenchmarkQueue_Put-12                                     6102608           206.1 ns/op        71 B/op          1 allocs/op
BenchmarkQueue_Get-12                                    30304675            45.30 ns/op       17 B/op          0 allocs/op
BenchmarkQueue_PutAndGet-12                              17171174            71.83 ns/op        7 B/op          0 allocs/op
BenchmarkQueue_Idempotent_Put-12                          1573570           706.9 ns/op       136 B/op          3 allocs/op
BenchmarkQueue_Idempotent_Get-12                          2275533           534.4 ns/op       105 B/op          0 allocs/op
BenchmarkQueue_Idempotent_PutAndGet-12                    2551188           494.5 ns/op        75 B/op          1 allocs/op
BenchmarkRateLimitingQueue_Put-12                         5852602           214.0 ns/op        71 B/op          1 allocs/op
BenchmarkRateLimitingQueue_PutWithLimited-12              1412991           852.6 ns/op       135 B/op          2 allocs/op
BenchmarkRateLimitingQueue_Get-12                        28186063            49.60 ns/op       19 B/op          0 allocs/op
BenchmarkRateLimitingQueue_PutAndGet-12                  15600679            75.69 ns/op        7 B/op          0 allocs/op
BenchmarkRateLimitingQueue_PutWithLimitedAndGet-12        1395084           855.5 ns/op       135 B/op          2 allocs/op

项目介绍

WorkQueue github.com/shengyanli1…

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 包。这个包提供了创建和管理工作队列的所有必要功能。

    import wkq "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 包来创建和管理工作队列:

演示代码

package main

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

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

// consumer 函数是一个消费者函数,它从队列中获取元素并处理它们
// The consumer function is a consumer function that gets elements from the queue and processes them
func consumer(queue wkq.Queue, wg *sync.WaitGroup) {
	// 当函数返回时,调用 wg.Done() 来通知 WaitGroup 一个任务已经完成
	// When the function returns, call wg.Done() to notify the WaitGroup that a task has been completed
	defer wg.Done()

	// 无限循环,直到函数返回
	// Infinite loop until the function returns
	for {
		// 从队列中获取一个元素
		// Get an element from the queue
		element, err := queue.Get()

		// 如果获取元素时发生错误,则处理错误
		// If an error occurs when getting the element, handle the error
		if err != nil {
			// 如果错误不是因为队列为空,则打印错误并返回
			// If the error is not because the queue is empty, print the error and return
			if !errors.Is(err, wkq.ErrQueueIsEmpty) {
				fmt.Println(err)
				return
			} else {
				// 如果错误是因为队列为空,则继续循环
				// If the error is because the queue is empty, continue the loop
				continue
			}
		}

		// 打印获取到的元素
		// Print the obtained element
		fmt.Println("> get element:", element)

		// 标记元素为已处理,'Done' 是在 'Get' 之后必需的
		// Mark the element as done, 'Done' is required after 'Get'
		queue.Done(element)
	}
}

func main() {
	// 创建一个 WaitGroup,用于等待所有的 goroutine 完成
	// Create a WaitGroup to wait for all goroutines to complete
	wg := sync.WaitGroup{}

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

	// 增加 WaitGroup 的计数器
	// Increase the counter of the WaitGroup
	wg.Add(1)

	// 启动一个新的 goroutine 来运行 comsumer 函数
	// Start a new goroutine to run the comsumer function
	go consumer(queue, &wg)

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

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

	// 等待一秒钟,让 comsumer 有机会处理队列中的元素
	// Wait for a second to give the comsumer a chance to process the elements in the queue
	time.Sleep(time.Second)

	// 关闭队列
	// Shut down the queue
	queue.Shutdown()

	// 等待所有的 goroutine 完成
	// Wait for all goroutines to complete
	wg.Wait()
}

运行结果

$ go run demo.go
> get element: hello
> get element: world
queue is shutting down

总结

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

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

  • 简单:尽可能保持简单和直观,避免复杂化。
  • 高效:确保性能优越,能够高效地完成任务。
  • 稳定:保证系统的稳定性,能够在各种情况下正常运行。
  • 易用:提供良好的用户体验,使用户能够轻松上手。
  • 可扩展:具备良好的扩展性,能够适应未来的需求变化。
  • 可维护:代码结构清晰,易于维护和更新。
  • 高质量:确保代码质量,减少错误和漏洞。

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

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