C++实现延时队列

1 延时队列简介

延时队列是一种特殊的消息队列,它允许将消息在一定的延迟时间后再进行消费。延时队列的主要特点是可以延迟消息的处理时间,以满足定时任务或者定时事件的需求。

延时队列的实现方式可以有多种,下面以使用消息中间件为例进行介绍。

在使用消息中间件实现延时队列时,通常会有以下几个关键组件:

  1. 消息生产者(Producer):负责将延时消息发送到队列中,通常会设置消息的延迟时间。

  2. 延时队列(Delay Queue):是一个存储延时消息的队列,消息在进入队列后会根据设置的延迟时间进行暂时存储。

  3. 消息消费者(Consumer):负责从延时队列中获取到达延迟时间的消息,并进行相应的处理。

  4. 消息中间件(Message Broker):提供消息传递和存储的功能,可以使用常见的消息中间件如RabbitMQ、Kafka等。

延时队列的工作流程如下:

  1. 消息生产者将延时消息发送到延时队列,同时设置消息的延迟时间。

  2. 延时队列将消息暂时存储在队列中,并根据设置的延迟时间进行排序。

  3. 消息消费者定期轮询延时队列,获取到达延迟时间的消息。

  4. 消息消费者处理延时消息,执行相应的任务或者处理定时事件。

延时队列的应用场景非常广泛,包括但不限于以下几个方面:

  1. 定时任务:可以将需要在特定时间执行的任务封装为延时消息,通过延时队列来触发任务的执行。

  2. 订单超时处理:可以将订单消息发送到延时队列中,并设置订单的超时时间,超过时间后,消费者从队列中获取到超时的订单消息,进行相应的处理。

  3. 消息重试机制:当某个消息处理失败时,可以将该消息发送到延时队列中,并设置一定的重试时间,超过时间后再次尝试处理。

总之,延时队列通过延迟消息的消费时间,提供了一种方便、可靠的方式来处理定时任务和定时事件。它在分布式系统中具有重要的作用,能够提高系统的可靠性和性能。

2 实现一个延时队列

延时队列的实现方式有很多种,例如基于Kafka中的时间轮、基于redis中的sorted set(有序集合)和SkipList(跳表),基于redis键过期回调,基于Quartz定时任务,基于JDK内置的DelayQueue和优先级队列,以及现在使用较多的MQ(如RabbitMQ)。这些方式都可以实现延时队列,对任务或消息进行延时处理等功能。

不过,本次介绍的是一种不依赖于第三方库,仅基于标准C++自身及其库的实现方式。同样能够实现延时队列进行消息或任务延时处理的效果。

3 开发环境

Windows 10
Visual Studio 2022
C++ 14标准(VS2022默认标准)

4 实现思路

  1. 首先定义了一个线程池类ThreadPool,用于管理一组线程的执行。在构造函数中创建了指定数量的线程,并使用lambda函数作为线程的执行体。每个线程会不断从任务队列中取出任务并执行,直到线程池被销毁。
  2. 线程池中的任务队列使用std::queue来存储任务。通过互斥锁和条件变量来实现线程间的同步和通信。当任务队列为空时,线程会进入等待状态,直到有新的任务加入或线程池被销毁。
  3. 线程池的enqueue函数用于向任务队列中添加新的任务。首先获取互斥锁,然后将任务加入队列。如果当前没有空闲线程,会创建一个新的线程并加入线程池。如果有空闲线程,会唤醒其中一个线程来执行任务。
  4. 接下来定义了一个自定义的比较器CompareTasks,用于比较延时任务的执行时间。在DelayedQueue类中,使用std::priority_queue来存储延时任务。优先队列会根据比较器的定义来自动排序,使得最早执行的任务位于队首。
  5. DelayedQueue类中的addTask函数用于向延时队列中添加新的任务。首先获取互斥锁,然后将任务和延时时间加入优先队列。最后通过条件变量唤醒工作线程。
  6. processTasks函数是工作线程的执行体,不断从优先队列中取出最早执行的任务。如果当前时间已经超过了任务的执行时间,则将任务加入线程池中执行。如果还未到执行时间,则等待到任务的执行时间再继续。
  7. 在main函数中,创建了一个DelayedQueue对象,并向队列中添加了三个延时任务。然后通过std::this_thread::sleep_for函数等待4秒,以保证所有任务都能执行完毕。

5 实现步骤

  1. 定义一个线程池类ThreadPool,用于管理一组线程的执行。
  2. 在构造函数中创建指定数量的线程,并使用lambda函数作为线程的执行体。
  3. 每个线程循环执行以下步骤:
    • 获取互斥锁,进入临界区。
    • 等待条件变量,直到任务队列不为空或线程池被销毁。
    • 如果线程池被销毁且任务队列为空,退出线程。
    • 如果任务队列不为空,取出队首任务并执行。
    • 释放互斥锁,退出临界区。
  4. 定义一个DelayedQueue类,用于实现延时队列。
  5. 在DelayedQueue类中,使用ThreadPool类作为成员变量,用于执行任务。
  6. 定义一个自定义的比较器CompareTasks,用于比较延时任务的执行时间。
  7. 在DelayedQueue类中,定义一个优先队列tasks,用于存储延时任务。
  8. 定义一个互斥锁mutex和条件变量condition,用于线程间的同步和通信。
  9. 定义一个原子布尔变量running,用于标识线程池是否在运行。
  10. 定义一个工作线程worker,用于执行任务队列中的任务。
  11. 在DelayedQueue类中,定义一个addTask函数,用于向延时队列中添加新的任务。
  12. 在addTask函数中,获取互斥锁,将任务和延时时间加入优先队列。
  13. 通过条件变量唤醒工作线程。
  14. 定义一个processTasks函数,作为工作线程的执行体。
  15. 在processTasks函数中,循环执行以下步骤:
    • 获取互斥锁,进入临界区。
    • 如果任务队列为空,等待条件变量。
    • 如果任务队列不为空,获取当前时间。
    • 如果当前时间已经超过了队首任务的执行时间,取出任务并加入线程池执行。
    • 如果当前时间还未到达队首任务的执行时间,等待到任务的执行时间再继续。
    • 释放互斥锁,退出临界区。
  16. 在main函数中,创建一个DelayedQueue对象,指定线程池的线程数量。
  17. 使用addTask函数向延时队列中添加延时任务。
  18. 使用std::this_thread::sleep_for函数等待足够的时间,以保证所有任务都能执行完毕。
  19. 返回0,结束程序的执行。

6 详细说明

6.1 类图

类图

6.2 模块说明

1.实现了一个延时队列和线程池,用于执行延时任务。

2.线程池模块:

  • ThreadPool类是一个线程池,用于管理线程和执行任务。
  • 成员变量:
    • workers: 线程数组,用于存储线程对象。
    • tasks: 任务队列,用于存储待执行的任务。
    • mutex: 互斥锁,用于保护任务队列的访问。
    • condition: 条件变量,用于线程间的同步。
    • running: 原子操作,表示线程池是否正在运行。
    • idleThreads: 原子操作,表示空闲线程数。
  • 构造函数ThreadPool(size_t numThreads):创建指定数量的线程,并将它们加入线程数组中。
  • 析构函数~ThreadPool():停止线程池运行,等待所有线程结束。
  • 成员函数EnQueue(F&& task):将任务添加到任务队列中,如果没有空闲线程,则创建新的线程。
  • EnQueue函数使用了模板,接受一个类型为F的模板参数,用于支持不同类型的任务。

3.延时队列模块:

  • DelayedQueue类是一个延时队列,用于管理延时任务的执行。
  • 成员变量:
    • threadPool: 线程池对象,用于执行延时任务。
    • tasks: 延时任务队列,用于存储待执行的延时任务。
    • mutex: 互斥锁,用于保护延时任务队列的访问。
    • condition: 条件变量,用于线程间的同步。
    • running: 原子操作,表示延时队列是否正在运行。
    • worker: 工作线程,用于处理延时任务。
  • 构造函数DelayedQueue(size_t numThreads):创建指定数量的线程的线程池,并创建工作线程。
  • 析构函数~DelayedQueue():停止延时队列运行,等待工作线程结束。
  • 成员函数AddTask(F&& task, std::chrono::milliseconds delay):将延时任务添加到延时任务队列中。
  • AddTask函数使用了模板,接受一个类型为F的模板参数,用于支持不同类型的任务。
  • AddTask函数还接受一个延时参数,用于指定任务的延时时间。

4.自定义比较器模块:

  • CompareTasks是一个自定义比较器,用于比较延时任务的执行时间。

6.3 数据结构

ThreadPool:

  • workers: vector // 线程数组,存储工作线程
  • tasks: queue // 任务队列,存储待执行的任务
  • mutex: mutex // 互斥锁,用于保护对任务队列的访问
  • condition: condition_variable // 条件变量,用于线程间的同步和通信
  • running: atomic // 原子操作,表示线程池是否正在运行
  • idleThreads: atomic // 原子操作,表示空闲线程数

CompareTasks:

  • operator()(lhs: pair, rhs: pair): bool // 自定义比较器,用于比较任务的执行时间

DelayedQueue:

  • threadPool: ThreadPool // 线程池,用于执行任务
  • tasks: priority_queue<pair, vector, CompareTasks> // 延时任务队列,按照任务的执行时间进行排序
  • mutex: mutex // 互斥锁,用于保护对延时任务队列的访问
  • condition: condition_variable // 条件变量,用于线程间的同步和通信
  • running: atomic // 原子操作,表示延时队列是否正在运行
  • worker: thread // 工作线程,用于处理延时任务

std::chrono::ThreadPool:

  • ThreadPool(numThreads: size_t) // 构造函数,创建指定数量的线程
  • ~ThreadPool() // 析构函数,停止线程池并等待所有线程结束
  • EnQueue(task: F&&) // 将任务添加到任务队列

std::chrono::CompareTasks:

  • operator()(lhs: pair, rhs: pair): bool // 比较任务的执行时间,返回较大的时间

std::chrono::DelayedQueue:

  • DelayedQueue(numThreads: size_t) // 构造函数,创建线程池和工作线程
  • ~DelayedQueue() // 析构函数,停止延时队列并等待工作线程结束
  • AddTask(task: F&&, delay: milliseconds) // 添加延时任务
  • ProcessTasks() // 处理延时任务

Main:

  • queue: std::chrono::DelayedQueue // 延时队列
  • main() // 主函数,创建延时队列并添加延时任务
  • task1: function<void()> // 延时任务1
  • task2: function<void()> // 延时任务2
  • task3: function<void()> // 延时任务3
  • delay1: milliseconds // 延时时间1
  • delay2: milliseconds // 延时时间2
  • delay3: milliseconds // 延时时间3

6.4 性能分析

性能分析:
1. 线程池的设计可以提高任务的并发执行能力,通过多线程执行任务可以提高整体的处理速度。
2. 延时队列的设计可以按照指定的延时时间执行任务,避免了在指定时间之前执行任务的情况。
时间复杂度分析:
1. 线程池的任务添加操作(EnQueue)的时间复杂度为O(1),因为只需要将任务添加到任务队列中。
2. 延时队列的任务添加操作(AddTask)的时间复杂度为O(log n),因为需要将任务添加到优先队列中,优先队列的插入操作的时间复杂度为O(log n),其中 n 是队列中的任务数量。
3. 线程池的任务执行操作的时间复杂度取决于任务的具体实现,无法确定具体的时间复杂度。
空间复杂度分析:
1. 线程池的空间复杂度主要取决于线程数组(workers)和任务队列(tasks)的大小,分别为O(numThreads)和O(m),其中 numThreads 是线程池的线程数量,m 是任务队列的任务数量。
2. 延时队列的空间复杂度主要取决于优先队列(tasks)的大小,为O(n),其中 n 是优先队列的任务数量。
小结:
1. 线程池和延时队列的设计可以提高任务的并发执行能力和按照延时时间执行任务的能力。
2. 时间复杂度方面,任务添加操作的时间复杂度较低,任务执行操作的时间复杂度取决于具体的任务实现。
3. 空间复杂度方面,线程池和延时队列的空间复杂度较低,取决于线程数量和任务数量。
4. 性能方面,线程池和延时队列的设计可以提高任务的处理效率和灵活性。

7 源码

/**********************************************************************
 * @file:   dq_cpp.cpp
 * @brief:  实现了一个延时队列和线程池。延时队列可以添加延时任务,并在指定时间后执行任务。
 * 线程池可以接收任务并在多个线程中执行。
 * 
 * @details: 定义了一个线程池类和一个延时队列类,用于管理多个线程执行任务。
 * 线程池类实现了线程的创建、任务的添加和执行,以及线程池的停止。
 * 延时队列类实现了延时任务的添加和执行,使用优先队列按照任务的执行时间进行排序。
 * 代码中使用了互斥锁和条件变量来实现任务的同步和等待。
 * 延时队列使用了优先队列来按照任务的执行时间进行排序,保证最早执行的任务能够先被执行。
 * 
 * @version:    1.0
 * @author: Jacky Zou
 * @date:   2023年08月22日
 *********************************************************************/
#include <iostream>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <chrono>
#include <functional>
#include <vector>
#include <atomic> // 引入原子操作库

namespace std {
namespace chrono {

class ThreadPool { // 定义线程池类
public:
    /**
     * @function:  ThreadPool
     * @brief:  构造函数,创建指定数量的线程池,初始化成员变量
     * 
     * @param:  numThreads 线程池中的线程数量
     * @author: Jacky Zou
     * @date:   2023年08月22日
     */
    ThreadPool(size_t numThreads) : running(true), idleThreads(0)
    {
        for (size_t i = 0; i < numThreads; ++i) { // 创建指定数量的线程
            workers.emplace_back([this]() { // 使用lambda函数创建线程
                while (running) { // 线程一直运行
                    std::function<void()> task; // 定义任务
                    {
                        std::unique_lock<std::mutex> lock(mutex); // 加锁
                        // 等待条件满足
                        condition.wait(lock, [this]() { return !running || !tasks.empty() || idleThreads > 0; });
                        if (!running && tasks.empty()) { // 如果线程池停止且任务队列为空
                            return;
                        }
                        if (!tasks.empty()) { // 如果任务队列不为空
                            task = std::move(tasks.front()); // 取出任务
                            tasks.pop(); // 弹出任务
                        } else { // 如果任务队列为空
                            idleThreads--; // 空闲线程数减一
                            continue; // 继续循环
                        }
                    }
                    task(); // 执行任务
                }
            });
        }
    }

    /**
     * @function:  ~ThreadPool
     * @brief:  析构函数
     * 
     * @author: Jacky Zou
     * @date:   2023年08月22日
     */
    ~ThreadPool()
    {
        {
            std::unique_lock<std::mutex> lock(mutex); // 加锁
            running = false; // 停止线程池
            condition.notify_all(); // 通知所有线程
        }

        for (std::thread& worker : workers) { // 遍历所有线程
            worker.join(); // 等待线程结束
        }
    }

    /**
     * @function:  EnQueue
     * @brief:  添加任务到队列
     * 
     * @tparam: F 任务类型
     * @param:  task 要添加的任务
     * @author: Jacky Zou
     * @date:   2023年08月22日
     */
    template<typename F> // 模板声明,接受一个类型为F的模板参数
    void EnQueue(F&& task)
    {
        std::unique_lock<std::mutex> lock(mutex); // 创建一个互斥锁,并加锁
        tasks.emplace(std::forward<F>(task)); // 将参数task转发给emplace函数,将任务添加到任务队列中
        if (idleThreads == 0) { // 如果没有空闲线程 
            workers.emplace_back([this]() { // 创建新的线程
                std::function<void()> task; // 定义任务
                while (running) { // 线程一直运行
                    {
                        std::unique_lock<std::mutex> lock(mutex); // 加锁
                        idleThreads++; // 空闲线程数加一
                        condition.wait(lock, [this]() { return !running || !tasks.empty(); }); // 等待条件满足
                        if (!running && tasks.empty()) { // 如果线程池停止且任务队列为空
                            return;
                        }
                        task = std::move(tasks.front()); // 取出任务
                        tasks.pop(); // 弹出任务
                        idleThreads--; // 空闲线程数减一
                    }
                    task(); // 执行任务
                }
            });
        } else { // 如果有空闲线程
            condition.notify_one(); // 通知一个线程
        }
    }

private:
    std::vector<std::thread> workers; // 线程数组
    std::queue<std::function<void()>> tasks; // 任务队列
    std::mutex mutex; // 互斥锁
    std::condition_variable condition; // 条件变量
    std::atomic<bool> running; // 原子操作,表示线程池是否正在运行
    std::atomic<int> idleThreads; // 原子操作,表示空闲线程数
};

/**
 * @brief: 自定义的比较函数对象,用于比较两个任务的执行时间
 *
 * @details: 该函数对象重载了函数调用运算符,接受两个任务对象作为参数,并根据任务的执行时间进行比较。
 *          返回值为 true 表示第一个任务的执行时间较晚,返回值为 false 表示第二个任务的执行时间较晚。
 *
 * @tparam: T1 第一个任务的类型,包括执行时间和任务函数
 * @tparam: T2 第二个任务的类型,包括执行时间和任务函数
 *
 * @param: lhs 第一个任务对象
 * @param: rhs 第二个任务对象
 *
 * @return: true 表示第一个任务的执行时间较晚,false 表示第二个任务的执行时间较晚
 * @author: Jacky Zou
 * @date:   2023年08月22日
 */
struct CompareTasks {
    bool operator()(const std::pair<std::chrono::time_point<std::chrono::steady_clock>, std::function<void()>>& lhs,
        const std::pair<std::chrono::time_point<std::chrono::steady_clock>, std::function<void()>>& rhs) const
    {
        return lhs.first > rhs.first; // 比较任务的执行时间,返回较大的时间
    }
};

/**
 * @brief: 定义延时队列类
 */
class DelayedQueue {
public:
    /**
     * @function:  DelayedQueue
     * @brief:  构造函数,创建线程池和工作线程
     * 
     * @param:  numThreads 线程数量
     * @author: Jacky Zou
     * @date:   2023年08月22日
     */
    DelayedQueue(size_t numThreads) : threadPool(numThreads), running(true),
        worker(&DelayedQueue::ProcessTasks, this) {}

    /**
     * @function:  ~DelayedQueue
     * @brief:  析构函数
     * 
     * @author: Jacky Zou
     * @date:   2023年08月22日
     */
    ~DelayedQueue()
    {
        {
            std::unique_lock<std::mutex> lock(mutex); // 加锁
            running = false; // 停止延时队列
            condition.notify_all(); // 通知所有线程
        }
        worker.join(); // 等待工作线程结束
    }

    /**
     * @function:  AddTask
     * @brief:  添加延时任务
     * 
     * @param:  task 任务
     * @param:  delay 延时时间
     * @author: Jacky Zou
     * @date:   2023年08月22日
     */
    template<typename F> // 模板声明,接受一个类型为F的模板参数
    void AddTask(F&& task, std::chrono::milliseconds delay)
    {
        std::unique_lock<std::mutex> lock(mutex); // 加锁
        // 将任务和执行时间添加到优先队列中
        tasks.push(std::make_pair(std::chrono::steady_clock::now() + delay, std::forward<F>(task)));
        condition.notify_one(); // 通知一个线程
    }

private:
    ThreadPool threadPool; // 创建线程池
    /**
     * @brief 优先队列,用于存储任务对象
     *
     * @details 该优先队列中的元素为 std::pair 对象,包括任务的执行时间和任务函数。
     *          优先队列会根据任务的执行时间进行排序,较早执行的任务会在队列的前面。
     *          使用自定义的比较函数对象 CompareTasks 进行元素的比较和排序。
     *
     * @tparam T1 任务对象的类型,包括执行时间和任务函数
     * @tparam T2 容器类型,用于存储任务对象
     * @tparam T3 比较函数对象类型,用于比较任务对象的执行时间
     */
    std::priority_queue<std::pair<std::chrono::time_point<std::chrono::steady_clock>, std::function<void()>>,
        std::vector<std::pair<std::chrono::time_point<std::chrono::steady_clock>, std::function<void()>>>,
            CompareTasks> tasks;

    std::mutex mutex; // 互斥锁
    std::condition_variable condition; // 条件变量
    std::atomic<bool> running; // 原子操作,表示延时队列是否正在运行
    std::thread worker; // 工作线程

    /**
     * @function:  ProcessTasks
     * @brief:  处理延时任务
     * 
     * @author: Jacky Zou
     * @date:   2023年08月22日
     */
    void ProcessTasks()
    {
        while (running) { // 工作线程一直运行
            std::unique_lock<std::mutex> lock(mutex); // 加锁
            if (tasks.empty()) { // 如果延时任务队列为空
                condition.wait(lock); // 等待条件满足
            } else { // 如果延时任务队列不为空
                std::pair<std::chrono::time_point<std::chrono::steady_clock>, std::function<void()>> task = tasks.top();
                auto now = std::chrono::steady_clock::now(); // 获取当前时间
                if (now >= task.first) { // 如果当前时间大于等于最早执行时间
                    auto task = tasks.top().second; // 取出任务
                    tasks.pop(); // 弹出任务
                    lock.unlock(); // 解锁
                    threadPool.EnQueue(std::move(task)); // 将任务添加到线程池中执行
                } else { // 如果当前时间小于最早执行时间
                    condition.wait_until(lock, task.first); // 等待直到最早执行时间
                }
            }
        }
    }
};

} // namespace chrono
} // namespace std

/**
 * @function:  main
 * @brief:  创建线程池,添加延时任务,设置等待时间
 * 
 * @return: 0
 * @author: Jacky Zou
 * @date:   2023年08月22日
 */
int main()
{
    std::chrono::DelayedQueue queue(4); // 创建4个线程的线程池

    // 添加延时任务
    queue.AddTask([]() { std::cout << "任务1已执行!" << std::endl; }, std::chrono::seconds(3));
    queue.AddTask([]() { std::cout << "任务2已执行!" << std::endl; }, std::chrono::seconds(6));
    queue.AddTask([]() { std::cout << "任务3已执行!" << std::endl; }, std::chrono::seconds(9));

    std::this_thread::sleep_for(std::chrono::seconds(10)); // 等待10秒

    return 0;
}

运行结果:
运行结果图

8 总结

利用C++语言,不基于第三方库实现了一个简单的延时队列,可以对任务进行延时处理。

PS:如有错误,欢迎指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

比特熊猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值