【c++】线程池的原理及实现

在这里插入图片描述


📄前言

不知道各位是否有试过点进限时抽奖网站、抢票网站呢?你是否好奇过一个网站、游戏是如何实现数十万、百万用户一起进行访问呢? 其实这类软件的背后的服务器总是离不开一个叫做线程池的设计,而这就是本文将讲解的内容,学习如何设计线程池,是每个后端程序猿的必修课之一。

线程池的原理

概念

当我们在一台多核CPU机器上使用多线程,无疑能够提高程序的性能,但不是每台机器的CPU都是“线程撕裂者”,我们无法给每个任务都分配一条线程,过度分配线程只会让程序性能下降。为此,我们需要给线程与需要执行的任务进行集中管理,减少线程创建和销毁所造成的开销,而这就是线程池。

线程池也是池化技术的一种,即将资源提前分配好,集中管理,当需要使用的时候在去使用,从而提高程序的效率。

  • 线程池的优点:

    1. 提高程序可维护性:线程池作为一个模块,降低了代码的耦合性,使得程序的可维护性得到提高
    2. 减少系统开销:避免频繁地创建和销毁资源,减少了系统的开销,尤其是在高负载情况下更能体现出其效率。
    3. 提高性能:通过并发执行任务,从而提高程序的性能。

工作原理

线程池最基础的三个结构组成就是任务队列工作线程线程管理。其中线程池中的任务队列负责将用户的任务打包成合适的格式,等待工作线程来取走工作线程负责任务的并发执行,而线程管理则用于管理线程的创建、消亡。

其实,线程池也可以看作是一种消费者生产者模型

  • 生产者:用户负责生成任务并将它们交给任务队列。可以将这看作为发送一个资源请求。
  • 消费者:线程池负责消费者的角色,用于处理将任务队列中的任务(资源)。

请添加图片描述

介绍完了线程池的工作原理,那就简单的介绍下工作流程吧。

工作流程:

+----------------+       +---------------------+       +-------------------+       +-------------------+
| 线程池初始化     |       | 添加任务到任务队列    |       | 线程从队列中取任务  |       | 执行任务并返回线程池  |
| 创建线程         |  -->  | 所有任务入队列        |  -->  | 自动取出并执行任务   |  -->  | 线程等待下一任务       |
+----------------+       +---------------------+       +-------------------+       +-------------------+

线程池的实现

要实现一个不那么塑料的线程池,就得要对C++的包装器、异步函数工具、完美转发有所了解,如果你对它们不太了解,可以去观看我的上一篇文章

你知道如何快速实现一个简单、高效的线程池吗?那就是上github搜索线程池,然后找到一个最多人收藏的(✓)。本篇文章讲解的线程池是基于github上大佬的线程池 所修改的。如果要提高编程实力,请务必去多上github观看源码。

线程池的基础结构

这个线程池只实现了线程池所必备的三个功能,**任务队列、工作线程、线程管理。**这个线程池的实现不过百行,对于初学者来说比较友好,没有必要把焦点分散到其他地方,只需要集中于了解线程池本身的实现原理。

class ThreadPool {
public:
    static ThreadPool* getInstance(size_t n = 0);   //单例模式实现

    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args) //将任务推入队列
    -> std::future<typename std::invoke_result_t<F, Args...>>;	//c++17 invoke_result_t用于获取推导的函数返回值

    template<class F>   // 无参函数版本
    auto enqueue(F&& f)
    -> std::future<typename std::invoke_result_t<F>>;

    ~ThreadPool();

    ThreadPool(const ThreadPool&) = delete;	//防止拷贝
    ThreadPool& operator=(const ThreadPool&) = delete;
private:
    ThreadPool(size_t);

private:
    static ThreadPool* _instance;
    static std::once_flag _flag;   //用于初始化函数

    std::vector< std::thread > _workers;    // 工作线程
    std::queue< std::function<void()> > _tasks; // 任务队列
    std::mutex _queue_mutex;    // 用于保护任务队列的互斥锁
    std::condition_variable _condition; // 用于同步操作
    std::atomic<bool> _stop;
};

std::once_flag ThreadPool::_flag;	//静态成员要在类外初始化
ThreadPool* ThreadPool::_instance = nullptr;

任务队列的实现

任务队列的难点在与如何理解如何包装任务的,以及智能指针的理解。但抛开C++中这些繁杂的部分,其实也就是往队列中发送任务。

template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args) //将任务推入队列
-> std::future<std::invoke_result_t<F, Args...>>	//返回一个期待,用于获取函数运行结果/异常结果
{
    using result_type = std::invoke_result_t<F, Args...>;

    auto task = std::make_shared< std::packaged_task<result_type()> > (
            std::bind(std::forward<F>(f), std::forward<Args>(args)...)
            );  // 包装任务成为异步任务。使用shared_ptr来动态管理存亡。

    std::future<result_type> res = task->get_future();  //期待绑定task
    {
        std::lock_guard<std::mutex> lock(_queue_mutex); // 保护任务队列,
        if(_stop)   throw std::runtime_error("在停止的线程池中加入任务");
        _tasks.emplace([task] { (*task)(); });  //再次将任务包装成 function<void()>。
    }
    _condition.notify_one();    //通知线程有任务可以消费了
    return res;
}

//无参版本
template<class F>
auto ThreadPool::enqueue(F&& f)
-> std::future<typename std::invoke_result_t<F>> {
    using result_type = std::invoke_result_t<F>;

    auto task = std::make_shared<std::packaged_task<result_type()>> (
            std::forward<F>(f)
    );  // 包装任务成为异步任务。使用shared_ptr来动态管理存亡。

    std::future<result_type> res = task->get_future();  //期待绑定task
    {
        std::lock_guard<std::mutex> lock(_queue_mutex); // 保护任务队列,
        if(_stop)   throw std::runtime_error("在停止的线程池中加入任务");
        _tasks.emplace([task] { (*task)(); });  //再次将任务包装成 function<void()>。
    }
    _condition.notify_one();    //通知线程有任务可以消费了
    return res;
}

工作线程的实现

工作线程的实现相比任务队列而言较为简单,只要确保往任务队列中取数据是线程安全,以及正确地结束线程即可。

ThreadPool* ThreadPool::getInstance(size_t n)   //单例模式实现
{   //c++11 的call_once,可用于保证线程安全地初始化单例对象,从而杜绝双重检查语句。
    std::call_once(_flag, [n] { _instance = new ThreadPool(n); });
    return _instance;
}

ThreadPool::ThreadPool(size_t n)
{
    for(int i = 0; i < n; ++i)
    {
        _workers.emplace_back
        (
            [this]
            {
                for(;;)
                {
                    std::function<void()> task;
                    {
                        std::unique_lock<std::mutex> lock(_queue_mutex);    //保护任务队列
                        //当队列为空时,等待新任务
                        _condition.wait(lock, [this]{ return _stop || !_tasks.empty(); });
                        if(_stop && _tasks.empty()) return; // 线程池停止,且没有任务,则退出
                        task = std::move(_tasks.front()); // 从队列获取任务
                        _tasks.pop();
                    }
                    task(); // 并发执行任务
                }
            }
        );
    }
}

ThreadPool::~ThreadPool()
{
    _stop.store(true);	
    _condition.notify_all();
    for(auto& it : _workers)	//回收线程资源
        it.join(); 	
}

线程池的应用与拓展

线程池的设计就至此结束了,让我们来简单使用下做好的线程池吧。

int test(int num1, int num2)
{
    return num1 + num2;
}

int main() {
    httplib::Server server;
    ThreadPool* pool = ThreadPool::getInstance(5);
    std::vector<std::future<int>> results(10);

    for (int i = 0; i < 10; i++) {
        results[i] = pool->enqueue(test, i, i + 1);
    }

    for(auto& it : results)
        std::cout << it.get() << std::endl;

    return 0;
}

线程池的拓展

这个线程池只是一个非常基础的线程池实现,实际的线程池会比这复杂得多,如下为线程池常见的拓展方式:

  1. 实现动态调整线程数:针对不同的场景线程池能够自动释放、创建线程。
  2. 优先级任务队列:为了确保紧急任务能够快速执行,可以定义一个优先级任务队列。
  3. 线程池任务异常处理:确保单个任务异常不会影响整个程序。

📓总结

掌握如何编写一个高效、安全的线程池对一个后端程序员来讲就像是西方不能没有耶路撒冷,毕竟如果要开发一个高并发需求的程序,例如抢票网站,服务器速度更不上、或者出现了线程安全,到时候用户就要寄律师函了。

📜博客主页:主页

📫我的专栏:C++

📱我的github:github

  • 19
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: C语言线程池的代码实现可以在GitHub上找到很多开源项目,以下是其中一个例子: https://github.com/rxi/dyad 这是一个简单的C语言线程池实现,主要使用了POSIX线程库,代码非常简洁和易于理解。它包含一个pool结构体,用于管理线程池的状态和任务队列等信息。主要函数包括pool_init用于初始化线程池,pool_submit用于提交任务,pool_wait用于等待线程池执行完所有任务,pool_destroy用于销毁线程池。 使用这个线程池框架,只需要简单地定义一个任务函数,并通过pool_submit提交任务,即可由线程池中的线程来执行任务。线程池内部会自动调度任务,并根据设置的线程池大小控制并发执行的线程数。 这个线程池实现还提供了一些额外的功能,例如支持任务超时设置,可以在任务执行的一定时间内获取任务的返回结果,也可以设置任务的最大重试次数。 通过在GitHub上搜索"C thread pool"关键词,还可以找到其他很多C语言线程池实现,这些开源项目提供了完整的代码实现和详细的文档说明,可以根据个人需求选择使用。 ### 回答2: 线程池是一种用于管理和复用线程的技术,通过预先创建一组线程并将其放入池中,以便在需要的时候可以重复使用。这样可以避免频繁创建和销毁线程的开销,提高系统的性能和效率。 在GitHub上有很多关于线程池实现的代码库,我以下将以Java语言为例来介绍一个常见的线程池实现。 Java的线程池是通过`ThreadPoolExecutor`类来实现的。我们可以在GitHub上搜索"ThreadPoolExecutor"关键字,就会找到很多相关的代码库。 比如,一个名为"java线程池的简单实现"的代码库,这个库提供了一个简单的线程池实现,包括线程池类`MyThreadPoolExecutor`和任务类`MyTask`。通过查看代码,可以了解到该线程池实现了以下几个功能: 1. 创建线程池:通过`MyThreadPoolExecutor`类的构造函数可以指定线程池的大小和其他相关的参数。 2. 提交任务:通过调用`MyThreadPoolExecutor`类的`submit()`方法将任务提交到线程池中。 3. 执行任务:线程池会自动管理和调度线程,并调用任务的`run()`方法来执行任务。 4. 监控线程池:可以通过`MyThreadPoolExecutor`类提供的方法获取线程池的状态,比如当前活动的线程数、完成的任务数等。 5. 终止线程池:通过调用`MyThreadPoolExecutor`类的`shutdown()`方法可以优雅地关闭线程池,等待当前正在执行的任务完成后再关闭线程池。 这只是一个简单的线程池实现,如果对线程池实现原理和更高级的应用有兴趣,可以进一步了解和探索更多的线程池实现代码库。 ### 回答3: C语言中的线程池可以通过使用Pthreads库来实现。以下是一个简单的C语言线程池的代码实现,你可以在GitHub上找到完整的代码。 #include <stdio.h> #include <stdlib.h> #include <pthread.h> #define MAX_THREADS 10 #define MAX_QUEUE 1000 typedef struct { void (*function)(void *); // 线程执行的函数指针 void *argument; // 函数参数 } task_t; task_t task_queue[MAX_QUEUE]; int queue_size = 0; int head = 0; int tail = 0; pthread_mutex_t queue_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t queue_cond = PTHREAD_COND_INITIALIZER; pthread_t worker_threads[MAX_THREADS]; int pool_shutdown = 0; // 添加任务到线程池 void pool_add_task(void (*function)(void *), void *argument) { pthread_mutex_lock(&queue_mutex); if (queue_size >= MAX_QUEUE) { fprintf(stderr, "Warning: task queue is full, the task is dropped.\n"); pthread_mutex_unlock(&queue_mutex); return; } task_queue[tail].function = function; task_queue[tail].argument = argument; tail = (tail + 1) % MAX_QUEUE; queue_size++; pthread_cond_signal(&queue_cond); pthread_mutex_unlock(&queue_mutex); } // 线程池的工作线程函数 void *worker(void *arg) { while (1) { pthread_mutex_lock(&queue_mutex); while (queue_size == 0 && !pool_shutdown) { pthread_cond_wait(&queue_cond, &queue_mutex); } if (pool_shutdown) { pthread_mutex_unlock(&queue_mutex); pthread_exit(NULL); } void (*function)(void *) = task_queue[head].function; void *argument = task_queue[head].argument; head = (head + 1) % MAX_QUEUE; queue_size--; pthread_mutex_unlock(&queue_mutex); function(argument); } } // 初始化线程池 void pool_init() { int i; for (i = 0; i < MAX_THREADS; i++) { pthread_create(&worker_threads[i], NULL, worker, NULL); } } // 关闭线程池 void pool_shutdown() { int i; pool_shutdown = 1; pthread_mutex_lock(&queue_mutex); pthread_cond_broadcast(&queue_cond); pthread_mutex_unlock(&queue_mutex); for (i = 0; i < MAX_THREADS; i++) { pthread_join(worker_threads[i], NULL); } } // 测试函数 void print_number(void *arg) { int number = *((int *)arg); printf("Number: %d\n", number); } int main() { pool_init(); int i; for (i = 0; i < 100; i++) { int *number = malloc(sizeof(int)); *number = i; pool_add_task(print_number, (void *)number); } pool_shutdown(); return 0; } 这个线程池实现包括了添加任务到队列,工作线程从请求队列中获取任务并执行,还有线程池的初始化和关闭。你可以在任务函数中处理自己的逻辑,此处的示例是打印数字。注意,这只是一个简单的线程池实现,还有许多其他特性可以添加。你可以在GitHub上查找更多更完整的C语言线程池实现

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值