C++实现线程池

一、为什么要有线程池?

        早期的计算机采用的是单指令单数据(SISD)架构,即一次只执行一条指令,处理一份数据。这种串行执行的方式是计算机的基本工作原理,然而随着计算机的发展,程序所承载的作业越来越复杂,传统的串行执行极大的拉长了作业执行的时间。

        举个例子:制作一杯咖啡

        1、取杯子;

        2、磨咖啡豆;

        3、烧水;

        4、泡咖啡;

        5、加糖和奶;

        在传统的串行执行计算机中,上面的每个步骤都是一条指令,依次执行,不能在前一步没有执行完的情况下开始下一步。比如,你想在磨咖啡豆的过程中烧水是不允许的,必须磨完咖啡豆后才能执行烧水的过程。不难看出这样的效率是十分低下的,因此慢慢的发展出了多处理器系统、多核处理器和并行计算,计算机能够同时执行多个任务,从而实现并行执行。通过多处理器,可以在磨咖啡的同时烧水,这样可以提高我们的效率。

二、进程和线程

进程(Process):

  1. 定义:进程是一个独立的执行单位,每个进程都有自己的独立内存空间,包括代码、数据、堆栈等。进程之间彼此独立,不共享内存,通常通过进程间通信来交换数据。

  2. 资源分配:每个进程拥有自己的资源,如文件描述符、打开的文件、网络连接等。进程需要操作系统分配资源,并拥有独立的地址空间。

  3. 开销:创建和销毁进程的开销较大,包括分配和回收内存、初始化和销毁进程数据结构等。

  4. 稳定性:由于进程之间相互隔离,一个进程的崩溃通常不会影响其他进程。因此,进程较为稳定,但也较为消耗资源。

  5. 并发性:进程可以并发执行,多个进程可以在不同的处理器上并行运行,从而提高系统的并发性。

线程(Thread):

  1. 定义:线程是进程内的执行单元,多个线程共享同一进程的资源,包括内存空间和文件描述符。线程是轻量级的执行单位。

  2. 资源共享:线程之间共享进程的资源,它们可以访问相同的内存、文件和网络连接。这使得线程之间的通信更加高效,但也需要更小的开销。

  3. 开销:创建和销毁线程的开销较小,因为它们共享进程的资源,不需要分配独立的地址空间。

  4. 稳定性:由于线程共享进程的资源,一个线程的错误可能会影响整个进程,使得进程较不稳定。但线程的错误通常不会引起整个系统的崩溃。

  5. 并发性:线程允许多个线程在同一个进程内并发执行,它们可以协同工作,共同完成任务。

总结

  • 进程是独立的执行单元,线程是进程内的执行单元。
  • 进程之间相互隔离,线程之间共享进程的资源。
  • 创建和销毁进程的开销较大,创建和销毁线程的开销较小。
  • 进程通常更稳定,但消耗更多资源。线程更轻量,但不如进程稳定。
  • 进程可以并发执行,多个进程可以并行运行。线程在同一进程内并发执行。

        现在我们常用的都是线程池,应为进程的切换和创建销毁会消耗更多的性能。

三、C++代码实现线程池

线程池需要下面四个核心成员:

        1、任务队列,任务队列中存放需要线程执行的任务;

        2、互斥锁,由于任务队列中是临界资源,被多个线程访问,需要互斥锁保证安全性;

        3、条件变量,当任务队列不为空的时候或者需要停止线程池运行时唤醒线程;

        4、工作线程,负责不断从任务队列中取出任务并执行。

        5、线程池是否停止工作的标志;

下面是将要使用到的头文件:

#pragma once

#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <stdexcept>

下面是线程池的私有数据成员:

std::vector<std::thread> workers_;          // 工作线程
std::queue<std::function<void()>>tasks_;    // 任务队列,存放匿名函数
std::mutex queue_mutex_;                    // 任务队列的互斥锁
std::condition_variable condition_;         // 条件变量,用来唤醒工作线程
bool stop_;                                 // 线程池是否正在工作

1、构造函数

接下来说一下线程池的构造函数,构造函数接受一个整型变量,这个整型代表线程池中存放线程的数目,并在构造函数中将线程以匿名函数的方式存放到vector容器内部。线程会不断检测任务队列中是否有需要执行的任务,如果没有就一直被阻塞,直到任务队列中有数据,被条件变量唤醒,取出任务并执行。

ThreadPool(size_t size)
        :stop_(false)
    {
        for(size_t i=0;i<size;++i)
        {
            workers_.emplace_back([this]{
                for(;;)    // 效果等于while(true)循环
                {
                    std::function<void()> task;

                    {    // 这里的大括号是为了unique_lock出了大括号作用域后自动解锁
                        std::unique_lock<std::mutex> lock(this->queue_mutex_);
                        this->condition_.wait(lock,[this]{
                            return (this->stop_)||!this->tasks_.empty();
                        });
                        if(this->stop_&&this->tasks_.empty())
                            return;
                        task=std::move(this->tasks_.front());
                        this->tasks_.pop();
                    }
                    task();
                }
            });
        }
    }

std::function<void()> task;

这句代码声明了一个函数指针: 指向一个不接受任何参数、返回值为void型的函数,task是函数指针的名称,将可调用对象分配给task,然后通过task()来执行函数。

this->condition_.wait(lock,[this]{
        return (this->stop_)||!this->tasks_.empty();
});

这段代码只有当括号内的代码结果为真时才会继续执行后面的代码,否则会一直阻塞在这个函数中。将上面的代码翻译过来:只有当线程池已经停止运行或者任务队列不为空时才会执行后面的代码,否则就阻塞在这里等待任务队列中被添加新的任务后被条件变量唤醒。 

if(this->stop_&&this->tasks_.empty())
        return; 

这里如果线程池已经停止运行了并且任务队列不为空的话就直接返回,停止线程的运行,加入任务队列不为空的条件限制是为了执行完任务队列中的所有待执行任务,避免有任务未执行而线程池中的线程直接停止运行。 

否则就将任务队列队首的任务通过move移动函数将任务赋值给task函数指针。移动操作是一种高效的资源转移操作,它将对象的资源(如内存、文件句柄等)从一个对象转移到另一个对象,而不进行复制。然后将这个任务从任务队列中移除。之后通过task()的形式执行函数,这就是每个线程的任务。

总的来说:每个线程的任务就是不断监听任务队列,任务队列不为空就尝试获得锁,并取出任务执行。

2、向任务队列中添加任务

template <class F,class... Args>
auto enqueue(F&& f,Args&&... args)
    ->std::future<typename std::result_of<F(Args...)>::type>
{
    using return_type=typename std::result_of<F(Args...)>::type;

    auto task=std::make_shared<std::packaged_task<return_type()>>(
        std::bind(std::forward<F>(f),std::forward<Args>(args)...)
    );

    std::future<return_type> res=task->get_future();
    {
        std::unique_lock<std::mutex> lock(queue_mutex_);
        if(stop_)
            throw std::runtime_error("enqueue on stopped ThreadPool.");

        tasks_.emplace([task](){
            (*task)();
        });
    }
    condition_.notify_one();
    return res;
}

这个函数接受一个可调用的函数F和一系列参数,并返回一个std::future对象,用于异步获取可调用函数的执行结果或状态。

auto enqueue(F&& f,Args&&... args)
    ->std::future<typename std::result_of<F(Args...)>::type>

这里的auto是自动推导,常用方法如下面这样:

auto& ret:m_vector; 

其中,m_vector是vector容器,如果存放的类型是int,那么经过auto推导会得到int& ret:m_vector; 

然而auto无法推到返回值,因此有了后面的代码:

->std::future<typename std::result_of<F(Args...)>::type>

这里给出了函数的返回值类型,result_of函数是C++11引入的类型萃取工具,用于推断一个可调用对象的返回类型,使得编译器能够在不需要显示指定返回类型的情况下确定可调用对象的返回类型,->用于指定函数的返回类型,通过上述操作,auto就可以推导出函数的返回值类型了。

using return_type=typename std::result_of<F(Args...)>::type;

这句代码使用using来声明了返回值类型,用来定义返回值类型。

auto task=std::make_shared<std::packaged_task<return_type()>>(
        std::bind(std::forward<F>(f),std::forward<Args>(args)...)
);

 这里声明了一个类型为task的智能指针,拥有一个std::packaged_task对象,make_shared用来创建一个智能指针,确保对象不再被需要的时候被销毁。通过packaged_task对象封装可调用对象,然后用bind函数将可调用对象和和可变参数绑定,并通过forward完美转发保证值的类别不变。到这里,task指针已经拥有了任务的所有资源。

[task](){

        (*task)();
}

这里代码是将任务封装成std::packaged_task可调用对象(匿名函数)并添加到线程池的任务队列中,以便稍后被工作线程异步执行。最后通过条件变量唤醒一个线程来执行这个任务。

3、析构函数

~ThreadPool()
{
    {
        std::unique_lock<std::mutex> lock(queue_mutex_);
        stop_=true;
    }
    condition_.notify_all();
    for(std::thread& worker: workers_)
    {
        if(worker.joinable())
            worker.join();
    }
}

拥有锁后将线程池停止运行的标志设置为真,函数体的大括号也是为了在出了作用域后释放锁。之后唤醒所有的线程。然后检查线程是否满足joinable的条件,满足就调用join函数,使得主函数等待线程执行完毕。这是为了将任务队列中的所有任务全部执行完毕。避免漏掉任务队列中的任务。

 四、小建议

最后一点小建议:我并不知道用visual studio或者vs code多线程的调试方法,但是尝试过gdb调试,大伙可以写一个测试的demo来测试这个线程池。在gdb中有多线程的调试方法:

info threads

上面的指令可以查看当前程序所有的线程和线程id,然后通过下面的命令就可以跳转到具体的某个线程执行到了哪个代码,非常有用。

thread id (这里的id是线程的id,使用info threads会给出)

五、源码 

// ThreadPool.h

#pragma once

#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <stdexcept>

class ThreadPool
{
private:
    std::vector<std::thread> workers_;          // 工作线程
    std::queue<std::function<void()>>tasks_;    // 任务队列,存放匿名函数
    std::mutex queue_mutex_;                    // 任务队列的互斥锁
    std::condition_variable condition_;         // 条件变量,用来唤醒工作线程
    bool stop_;                                 // 线程池是否正在工作
public:
    ThreadPool(size_t size)
        :stop_(false)
    {
        for(size_t i=0;i<size;++i)
        {
            workers_.emplace_back([this]{
                for(;;)
                {
                    std::function<void()> task;

                    {
                        std::unique_lock<std::mutex> lock(this->queue_mutex_);
                        this->condition_.wait(lock,[this]{
                            return (this->stop_)||!this->tasks_.empty();
                        });
                        if(this->stop_&&this->tasks_.empty())
                            return;
                        task=std::move(this->tasks_.front());
                        this->tasks_.pop();
                    }
                    task();
                }
            });
        }
    }

    template <class F,class... Args>
    auto enqueue(F&& f,Args&&... args)
        ->std::future<typename std::result_of<F(Args...)>::type>
    {
        using return_type=typename std::result_of<F(Args...)>::type;

        auto task=std::make_shared<std::packaged_task<return_type()>>(
            std::bind(std::forward<F>(f),std::forward<Args>(args)...)
        );

        std::future<return_type> res=task->get_future();
        {
            std::unique_lock<std::mutex> lock(queue_mutex_);
            if(stop_)
                throw std::runtime_error("enqueue on stopped ThreadPool.");

            tasks_.emplace([task](){
                (*task)();
            });
        }
        condition_.notify_one();
        return res;
    }

    ~ThreadPool()
    {
        {
            std::unique_lock<std::mutex> lock(queue_mutex_);
            stop_=true;
        }

        condition_.notify_all();
        for(std::thread& worker: workers_)
        {
            if(worker.joinable())
                worker.join();
        }
    }
};
// TestDemo.cpp

#include <iostream>
#include <chrono>
#include "ThreadPool.h"

int main() {
    ThreadPool pool(4);
    std::vector<std::future<int>> results;

    for (int i = 0; i < 8; ++i) {
        results.emplace_back(pool.enqueue([i] {
            try {
                std::cout << "hello " << i << std::endl;
                std::this_thread::sleep_for(std::chrono::seconds(1));
                std::cout << "world " << i << std::endl;
                return i * i;
            } catch (const std::exception& e) {
                std::cerr << "Exception in task: " << e.what() << std::endl;
                return -1;  // or handle the exception in an appropriate way
            }
        }));
    }

    for (auto&& result : results) {
        int value = result.get();
        std::cout << "Result: " << value << std::endl;
    }

    return 0;
}
  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 实现线程池的方法有很多,但是最常见的方法是使用队列来维护任务。每个线程都在队列中等待任务,当有新任务到达时,就从队列中取出一个任务并执行。这样,每个线程都可以在并行执行任务,而不需要创建新的线程。 在C语言中实现线程池的代码如下: ``` #include <pthread.h> #include <stdio.h> #include <stdlib.h> #define NUM_THREADS 5 void *print_hello(void *threadid) { long tid; tid = (long)threadid; printf("Hello World! It's me, thread #%ld!\n", tid); pthread_exit(NULL); } int main(int argc, char *argv[]) { pthread_t threads[NUM_THREADS]; int rc; long t; for (t = 0; t < NUM_THREADS; t++) { printf("In main: creating thread %ld\n", t); rc = pthread_create(&threads[t], NULL, print_hello, (void *)t); if (rc) { printf("ERROR; return code from pthread_create() is %d\n", rc); exit(-1); } } pthread_exit(NULL); } ``` ### 回答2: 要实现线程池,首先需要先了解线程池的基本概念和原理。 线程池是一种用来管理和复用线程的技术,它能够维护一个线程队列,按需创建和销毁线程,并将任务分配给这些线程来执行。使用线程池可以提高程序的性能,减少线程创建和销毁的开销。 在C语言中,可以使用多线程的库来实现线程池,比如pthread库。下面是一个简单的用C语言实现线程池的步骤: 1. 定义线程池结构体:创建一个结构体来保存线程池的相关信息,如线程池的大小、任务队列、互斥锁、条件变量等。 2. 初始化线程池:在初始化函数中,需要对线程池中的各个成员进行初始化,如创建指定数量的线程、初始化互斥锁和条件变量等。 3. 定义任务函数:线程池的任务函数用于处理任务队列中的任务,根据具体需求来定义任务的执行逻辑。 4. 添加任务到线程池:当有新的任务时,将任务添加到任务队列中,并通过条件变量来通知线程池中的线程有新任务可执行。 5. 线程池中的线程获取任务并执行:在线程中循环检查任务队列,当有任务时,线程从任务队列中获取任务并执行。 6. 销毁线程池:在停止使用线程池时,要销毁线程池中的资源,包括线程的回收、互斥锁和条件变量的销毁等。 通过以上步骤,就可以在C语言中实现一个简单的线程池。具体实现中还需要考虑线程同步、任务队列的管理等问题,以确保线程池的稳定性和性能。 ### 回答3: 线程池是用来管理和复用线程的一种机制,可以更有效地使用系统资源和提高应用程序的性能。下面是使用C语言实现线程池的一般步骤: 1. 定义一个线程池的结构体,包含线程池的状态、大小、最大线程数、工作任务队列等信息。 ```c typedef struct { pthread_t *threads; // 线程数组 int thread_count; // 线程数 int max_threads; // 最大线程数 int pool_size; // 线程池大小 int shutdown; // 关闭标志 pthread_mutex_t mutex; // 互斥锁 pthread_cond_t notify; // 条件变量 Task *task_queue; // 任务队列 } ThreadPool; ``` 2. 初始化线程池,创建指定数量的线程。 ```c ThreadPool* thread_pool_init(int pool_size) { ThreadPool *pool = malloc(sizeof(ThreadPool)); pool->threads = malloc(pool_size * sizeof(pthread_t)); pool->thread_count = pool_size; pool->max_threads = pool_size; pool->pool_size = 0; pool->shutdown = 0; // 初始化互斥锁和条件变量 pthread_mutex_init(&(pool->mutex), NULL); pthread_cond_init(&(pool->notify), NULL); // 创建线程 for (int i = 0; i < pool_size; i++) { pthread_create(&(pool->threads[i]), NULL, thread_worker, (void*)pool); } return pool; } ``` 3. 定义线程工作函数,不断从任务队列中取出任务执行。 ```c void* thread_worker(void *arg) { ThreadPool *pool = (ThreadPool*)arg; while (1) { pthread_mutex_lock(&(pool->mutex)); // 线程池关闭,退出线程 while (pool->pool_size == 0 && !pool->shutdown) { pthread_cond_wait(&(pool->notify), &(pool->mutex)); } if (pool->shutdown) { pthread_mutex_unlock(&(pool->mutex)); pthread_exit(NULL); } // 从任务队列中取出任务执行 Task *task = pool->task_queue; pool->task_queue = pool->task_queue->next; pool->pool_size--; pthread_mutex_unlock(&(pool->mutex)); task->func(task->arg); free(task); } pthread_exit(NULL); } ``` 4. 定义任务结构体,包含任务函数指针和参数。 ```c typedef struct Task { void (*func)(void*); void *arg; struct Task *next; } Task; ``` 5. 向线程池中添加任务。 ```c void thread_pool_add_task(ThreadPool *pool, void (*func)(void*), void *arg) { Task *task = malloc(sizeof(Task)); task->func = func; task->arg = arg; task->next = NULL; pthread_mutex_lock(&(pool->mutex)); if (pool->task_queue == NULL) { pool->task_queue = task; } else { Task *cur = pool->task_queue; while (cur->next != NULL) { cur = cur->next; } cur->next = task; } pool->pool_size++; pthread_mutex_unlock(&(pool->mutex)); pthread_cond_signal(&(pool->notify)); } ``` 6. 关闭线程池。 ```c void thread_pool_shutdown(ThreadPool *pool) { if (pool == NULL) { return; } pthread_mutex_lock(&(pool->mutex)); pool->shutdown = 1; pthread_mutex_unlock(&(pool->mutex)); pthread_cond_broadcast(&(pool->notify)); // 等待线程退出 for (int i = 0; i < pool->thread_count; i++) { pthread_join(pool->threads[i], NULL); } // 释放资源 free(pool->threads); while (pool->task_queue != NULL) { Task *next = pool->task_queue->next; free(pool->task_queue); pool->task_queue = next; } pthread_mutex_destroy(&(pool->mutex)); pthread_cond_destroy(&(pool->notify)); free(pool); } ``` 以上是一个简单的线程池实现,通过初始化线程池、添加任务、关闭线程池等操作,可以有效地管理和复用线程,提高应用程序的性能。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值