线程池的介绍以及【基于线程池的生产者消费者模型的模拟实现】

目录

线程池的介绍

基于线程池的生产者消费者模型的模拟实现

线程池类ThreadPool的模拟实现

ThreadPool类的成员变量

ThreadPool类的构造函数和全局的Routine函数

ThreadPool类的析构函数

ThreadPool类的pushTask函数

ThreadPool类的整体代码

Task.h文件的整体代码

testMain.cc文件的整体代码

对【基于线程池的生产者消费者模型的模拟实现】的测试


线程池的介绍

首先要知道的是,线程池和进程池一样也是一种池化技术。说简单点就是不要等待若干任务来了再创建若干线程去处理它们,处理完后又销毁这若干个线程;而是不管有无任务,在所有的逻辑开始前首先创建一批线程,等到一个任务过来,我就随便派一个线程去处理它,如果有其他任务,就再派另一个线程去处理它,这些线程处理完任务后不销毁,而是在线程池中等待下一次任务的派发。这样的做法,即线程池的做法会有很明显的优势:

  1. 避免在处理短时间任务时创建与销毁线程的代价以节省时间成本。
  2. 能够更快的开始处理任务从而让任务更快的结束,再次节省了时间成本。

然后要知道的是:其实从上一段就可以看出线程池本质就是一种生产者消费者模型。比如主线程就可以作为生产者去生产任务(虽然生产任务对象时所需数据可能来自于主线程,也可能来自于网络这样的外部来源,但生产者一定是主线程,而不可能是像网络这样的外部来源,并且将生产出的任务放进交易场所时也只能是由主线程放置的),根据生产者消费者模型的理论,主线程(即生产者)就把生产出的任务放进一个交易场所(即可以是通过条件变量实现的阻塞队列或者是通过POSIX信号量实现的环形队列)中,然后线程池里的所有线程都可以作为消费者,每次都从交易场所中获取任务后去处理任务。

(如果忘了生产者消费者模型、忘了通过条件变量实现的阻塞队列或者是通过POSIX信号量实现的环形队列,请分别回顾<<生产消费者模型的介绍以及其的模拟实现>><<POSIX信号量(包含通过POSIX信号量模拟实现的生产线程和消费线程并发运行的生产者消费者模型)>>

基于线程池的生产者消费者模型的模拟实现

线程池类ThreadPool的模拟实现

ThreadPool类的成员变量

上文中说过了,线程池就是一个生产者消费者模型,让主线程作为生产者去生产任务,把生产出的任务放进一个交易场所(其可以是通过条件变量实现的阻塞队列、也可以是通过信号量实现的环形队列,在当前模拟实现时咱们就选择前者),然后线程池里的所有线程作为消费者,每次都从交易场所中获取任务后去处理任务。

既然线程池ThreadPool是一个基于阻塞队列的生产者消费者模型,那么该类对象就一定有这些成员:

  • 阻塞队列queue<T> _task_queue,消费者(即线程池中的所有线程,注意线程池中的线程都是非主线程)都需要通过该阻塞队列获取任务;生产者(即主线程)需要往该阻塞队列中放置任务,本质上这个队列就是生产者消费者模型中的临界资源,形象点说叫交易场所。

  • pthread_mutex_t _lock,即分配给阻塞队列(即临界资源或者说交易场所)的锁。需要该锁的原因是需要维护【生产者和消费者的互斥关系】,即防止生产者和消费者同时访问阻塞队列,以避免发生【生产者放置任务的动作还只做到一半,消费者就跑进交易场所中接取任务导致接取到一个残缺的任务】和【消费者接取任务的动作还只做到一半,生产者就跑进交易场所中放置任务导致消费者正在接取的任务被覆盖了】。

  • pthread_cond_t _cond,即分配给消费者(线程池中的所有线程都是消费者)的条件变量,当生产者(即主线程)不断往任务队列中push时,每push一个任务都应该试图唤醒一个消费者线程,避免任务没有消费者接取从而全在任务队列中堆积。注意即使所有消费者线程都在工作,那我唤醒它也没关系,因为此时它会忽略这个唤醒信息,所以这里无脑唤醒即可。

  • vector<pthread_t*> _v。拿它作为线程池,里面存了所有线程的线程ID的地址。

  • int _num,用于统计_v中有多少个线程。

结合上面的理论,我们可以编写出以下代码。

#pragma once
#include<iostream>
using namespace std;
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<vector>
#include<queue>


template<class T>
class ThreadPool//用于处理T类型数据的线程池
{
public


private:
    vector<pthread_t*> _v;//线程池
    int _num;//统计_v中有多少个线程
    queue<T> _task_queue;//消费者(即线程池中的所有线程,注意线程池中的线程都是非主线程)都需要通过该任务队列获取任务;生产者(即主线程)需要往该任务队列中放置任务。所以本质上这个队列就是生产者消费者模型中的临界资源,形象点说叫交易场所
    pthread_mutex_t _lock;//分配给任务队列(即临界资源或者说交易场所)的锁,需要该锁的原因是需要维护【生产者和消费者的互斥关系】,避免发生【生产者放置任务的动作还只做到一半,消费者就跑进交易场所中接取任务】和【消费者接取任务的动作还只做到一半,生产者就跑进交易场所中放置任务】
    pthread_cond_t _cond;//分配给消费者(线程池中的所有线程都是消费者)的条件变量,当生产者(即主线程)不断往任务队列中push时,每push一个任务都应该试图唤醒一个线程,避免任务没有消费者接取从而全在任务队列中堆积。注意即使所有消费者线程都在工作,那我唤醒它也没关系,因为此时它会忽略这个唤醒信息,所以这里无脑唤醒即可。
};



ThreadPool类的构造函数和全局的Routine函数

构造函数的编写思路:

  • 既然ThreadPool类中有锁和条件变量的成员,那肯定是需要在其的构造函数中初始化它们的;
  • 然后要说的是何时创建线程让线程去执行线程函数。我们有两种选择,第一种是在构造函数中就创建线程,创建完毕后线程就自动开始执行线程函数了;第二是在其他函数中创建线程,假如该函数叫run函数,这样一来,我们就可以在手动调用run时再创建线程并让线程自动执行线程函数,能方便用户控制。说一下,第二种选择实际上是个不太必要的操作,因为线程池的理念就是在所有逻辑执行前先创建一批线程,这样当任务来时就能直接派发线程去处理它。而如果让我们手动控制,如果用户编写代码的逻辑出错了,即是等到任务来了后再调用run函数的,那就太慢了,虽然问题也不大,但相比于之前无疑是有瑕疵的。大家可以权衡一下自由选择,这里我们就采用第1种。

除了作为生产者的主线程(即执行main函数的线程),其他线程都是被pthread_create创建出来的新线程,这些新线程都是消费者线程,因为所有消费者线程都需要在阻塞队列中接取任务,而我们把这个接取任务的逻辑放在了全局的Routine函数中,所以所有的消费者线程的线程函数就都应该是Routine函数。Routine函数的编写思路是:

  • 既然Routine函数的逻辑是用于让所有消费者线程获取阻塞队列中的任务的,又因为阻塞队列同一时间内只能被一个线程访问,所以消费者线程从阻塞队列中获取任务时肯定是需要进行加锁以及获取任务完毕后的解锁的。
  • 在Routine函数中获取任务时还要注意临界区(即阻塞队列)中的资源是否就绪,如果阻塞队列中没有消费者线程关心的资源(即任务),那就需要让该消费者线程陷入阻塞(通过调用pthread_cond_wait);而如果有消费者线程关心的资源(即任务),那就让消费者线程获取该资源并完成消费。需要注意的是消费者线程获取资源完毕后,需要先解开分配给阻塞队列的锁,然后再消费,否则会造成所有消费者线程在处理任务时不能并发,这样多线程编程就失去了意义。然后要注意的是判断临界区的资源是否就绪时需要套一个while,而不是if,防止伪唤醒的情景发生,这些都是<<生产消费者模型的介绍以及其的模拟实现>>一文中老生常谈的内容。
  • 说一下,因为Routine函数不是ThreadPool类的成员函数,而是全局的函数,但又要在ThreadPool类外访问类内的成员,所以1、需要让Routine函数成为ThreadPool类的友元。2、需要在pthread_create创建消费者线程时把ThreadPool对象的this指针作为实参传给Routine函数的形参。
  • 因为Routine函数在ThreadPool类的上方,而Routine函数内是需要ThreadPool这个标识符的,所以需要前置声明一下ThreadPool类。
  • 说一下,因为线程的创建和销毁的成本太高,所以线程池中的线程(线程池中的所有线程都是消费者线程)是不能轻易的被创建和销毁的,所以不能在消费者线程接取并处理完一次任务后就让消费者线程函数Routine结束了,消费者线程是需要不断地从阻塞队列中获取任务的,处理完一个任务后再接下一个任务,如果没有任务就陷入阻塞,等待生产者的唤醒信息。所以根据这个理论,因为Routine是所有消费者线程的线程函数,用于从阻塞队列中接取和处理任务,所以Routine函数里面的逻辑一定是套上了while或者for循环的。

结合上面的理论,ThreadPool类的构造函数的代码如下。

#pragma once
#include<iostream>
using namespace std;
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<vector>
#include<queue>

template<class T>
class ThreadPool;//前置声明

//线程池中所有的线程(即消费者线程)的例行程序
template<class K>
void* routine(void* args)
{
    ThreadPool<K>* tp = (ThreadPool<K>*)args;
    cout<<"我是消费者线程,线程ID为:"<<pthread_self()<<",启动成功!"<<endl;
    K task;

    while(1)
    {
        //从任务队列中获取任务
        //消费者线程进入交易场所接取任务时先加锁
        pthread_mutex_lock(&(tp->_lock));
        //如果任务队列中为空,即没有任务时,那就让消费者线程在条件变量下陷入阻塞,等待资源就绪
        while(tp->_task_queue.empty() == true)
            pthread_cond_wait(&(tp->_cond), &tp->_lock);
        task = tp->_task_queue.front();
        tp->_task_queue.pop();
        //消费者线程接取完任务后再解锁并处理刚接取的任务
        pthread_mutex_unlock(&(tp->_lock));
        cout<<"消费者线程(线程池中的线程都是消费者线程)<"<<pthread_self()<<">解决的任务为: "<<task._x<<"+"<<task._y<<" = "<<task()<<endl;
        //注意和正常的生产者消费者模型不太一样的是:这里消费者处理完任务后不用pthread_cond_signal唤醒生产者,线程池的宗旨是有任务就处理,没任务就摸鱼,并不需要催促生产者
    }
}

template<class T>
class ThreadPool//用于处理T类型数据的线程池
{
    template<class K> friend void* routine(void* args);
public:
    ThreadPool(int num)//num表示线程池中需要多少线程
        :_num(num)
    {
        //是需要在构造函数中初始化锁的
        pthread_mutex_init(&_lock,nullptr);
        //是需要在构造函数中初始化条件变量的
        pthread_cond_init(&_cond,nullptr);
        for(int i=0;i<num;i++)
        {
            _v.push_back(new pthread_t);
        }

        for(pthread_t* x:_v)
        {
            //传this指针是因为在类外部的例行函数中需要访问类内的成员。
            pthread_create(x, nullptr, routine<T>, (void*)this);
        }
    }


private:
    vector<pthread_t*> _v;//线程池
    int _num;//统计_v中有多少个线程
    queue<T> _task_queue;//消费者(即线程池中的所有线程,注意线程池中的线程都是非主线程)都需要通过该任务队列获取任务;生产者(即主线程)需要往该任务队列中放置任务。所以本质上这个队列就是生产者消费者模型中的临界资源,形象点说叫交易场所
    pthread_mutex_t _lock;//分配给任务队列(即临界资源或者说交易场所)的锁,需要该锁的原因是需要维护【生产者和消费者的互斥关系】,避免发生【生产者放置任务的动作还只做到一半,消费者就跑进交易场所中接取任务】和【消费者接取任务的动作还只做到一半,生产者就跑进交易场所中放置任务】
    pthread_cond_t _cond;//分配给消费者(线程池中的所有线程都是消费者)的条件变量,当生产者(即主线程)不断往任务队列中push时,每push一个任务都应该试图唤醒一个线程,避免任务没有消费者接取从而全在任务队列中堆积。注意即使所有消费者线程都在工作,那我唤醒它也没关系,因为此时它会忽略这个唤醒信息,所以这里无脑唤醒即可。
};

ThreadPool类的析构函数

析构函数的编写思路:

  • 既然ThreadPool类中有锁和条件变量的成员,那肯定是需要在其的析构函数中销毁它们的;

  • 需要对创建出来的各个线程调用pthread_join函数进行线程等待,即让OS回收掉和线程相关的资源。要注意的是虽然join函数会释放绝大多数和线程相关的资源,但因为线程ID对象(即pthread_t类的对象)是在构造函数中new出来的在堆上的变量,所以需要手动delete释放。

结合上面的理论,ThreadPool类的析构函数的代码如下。

#pragma once
#include<iostream>
using namespace std;
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<vector>
#include<queue>


template<class T>
class ThreadPool//用于处理T类型数据的线程池
{
public
    ~ThreadPool()
    {
        for(vector<pthread_t*>::iterator it = _v.begin(); it != _v.end(); it++)
        {          
           //(*it)是vector中存的元素、注意每个元素只是线程ID的地址,而不是线程ID
            pthread_join(*(*it), nullptr);
            //join函数会释放绝大多数和线程相关的资源,但因为这里的线程ID对象是在构造函数中new出来的在堆上的变量,所以需要手动delete释放。
            delete (*it);
        }
        //是需要在析构函数中销毁锁的
        pthread_mutex_destroy(&_lock);
        //是需要在析构函数中销毁条件变量的
        pthread_cond_destroy(&_cond);
    }

private:
    vector<pthread_t*> _v;//线程池
    int _num;//统计_v中有多少个线程
    queue<T> _task_queue;//消费者(即线程池中的所有线程,注意线程池中的线程都是非主线程)都需要通过该任务队列获取任务;生产者(即主线程)需要往该任务队列中放置任务。所以本质上这个队列就是生产者消费者模型中的临界资源,形象点说叫交易场所
    pthread_mutex_t _lock;//分配给任务队列(即临界资源或者说交易场所)的锁,需要该锁的原因是需要维护【生产者和消费者的互斥关系】,避免发生【生产者放置任务的动作还只做到一半,消费者就跑进交易场所中接取任务】和【消费者接取任务的动作还只做到一半,生产者就跑进交易场所中放置任务】
    pthread_cond_t _cond;//分配给消费者(线程池中的所有线程都是消费者)的条件变量,当生产者(即主线程)不断往任务队列中push时,每push一个任务都应该试图唤醒一个线程,避免任务没有消费者接取从而全在任务队列中堆积。注意即使所有消费者线程都在工作,那我唤醒它也没关系,因为此时它会忽略这个唤醒信息,所以这里无脑唤醒即可。
};



ThreadPool类的pushTask函数

该函数是需要被主线程调用的,让作为生产者的主线程往ThreadPool对象中的阻塞队列成员_task_queue中放置任务,所以pushTask的思路为:

  • 既然是阻塞队列,那在所有线程中,同一时间内只能有一个线程访问阻塞队列,所以生产者线程即主线程往阻塞队列中放置任务时肯定是需要进行加锁以及放置完毕后的解锁的。
  • 主线程作为生产者,放置一个任务到阻塞队列后,它是知道临界资源(即阻塞队列)中一定有消费者关心的数据资源就绪的(因为即使在生产者本次放置数据之前阻塞队列就为空,但因为生产者刚刚才放置了一个数据,所以就一定有一个数据资源就绪),此时就应该由生产者去唤醒消费者线程从而让消费者线程运行起来去阻塞队列中获取数据(或者说任务),这样消费者之后才有数据(或者说任务)去消费。注意即使所有消费者线程都在工作,那生产者线程调用pthread_cond_signal唤醒某个消费者线程也没关系,因为此时消费者线程会忽略这个唤醒信息,所以这里无脑唤醒即可。

结合上面的理论,ThreadPool类的pushTask函数的代码如下。

#pragma once
#include<iostream>
using namespace std;
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<vector>
#include<queue>


template<class T>
class ThreadPool//用于处理T类型数据的线程池
{
public
    //该函数只被作为生产者的主线程调用
    void pushTask(const T& task)
    {
        pthread_mutex_lock(&_lock);
        _task_queue.push(task);
        pthread_mutex_unlock(&_lock);
        //生产者(即主线程)往交易场所中放置完任务后,需要唤醒某个消费者线程去接取任务
        pthread_cond_signal(&_cond);
    }
private:
    vector<pthread_t*> _v;//线程池
    int _num;//统计_v中有多少个线程
    queue<T> _task_queue;//消费者(即线程池中的所有线程,注意线程池中的线程都是非主线程)都需要通过该任务队列获取任务;生产者(即主线程)需要往该任务队列中放置任务。所以本质上这个队列就是生产者消费者模型中的临界资源,形象点说叫交易场所
    pthread_mutex_t _lock;//分配给任务队列(即临界资源或者说交易场所)的锁,需要该锁的原因是需要维护【生产者和消费者的互斥关系】,避免发生【生产者放置任务的动作还只做到一半,消费者就跑进交易场所中接取任务】和【消费者接取任务的动作还只做到一半,生产者就跑进交易场所中放置任务】
    pthread_cond_t _cond;//分配给消费者(线程池中的所有线程都是消费者)的条件变量,当生产者(即主线程)不断往任务队列中push时,每push一个任务都应该试图唤醒一个线程,避免任务没有消费者接取从而全在任务队列中堆积。注意即使所有消费者线程都在工作,那我唤醒它也没关系,因为此时它会忽略这个唤醒信息,所以这里无脑唤醒即可。
};



ThreadPool类的整体代码

以下是整个ThreadPool.h的代码。

#pragma once
#include<iostream>
using namespace std;
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<vector>
#include<queue>

template<class T>
class ThreadPool;//前置声明

//线程池中所有的线程(即消费者线程)的例行程序
template<class K>
void* routine(void* args)
{
    ThreadPool<K>* tp = (ThreadPool<K>*)args;
    cout<<"我是消费者线程,线程ID为:"<<pthread_self()<<",启动成功!"<<endl;
    K task;

    while(1)
    {
        //从任务队列中获取任务
        //消费者线程进入交易场所接取任务时先加锁
        pthread_mutex_lock(&(tp->_lock));
        //如果任务队列中为空,即没有任务时,那就让消费者线程在条件变量下陷入阻塞,等待资源就绪
        while(tp->_task_queue.empty() == true)
            pthread_cond_wait(&(tp->_cond), &tp->_lock);
        task = tp->_task_queue.front();
        tp->_task_queue.pop();
        //消费者线程接取完任务后再解锁并处理刚接取的任务
        pthread_mutex_unlock(&(tp->_lock));
        cout<<"消费者线程(线程池中的线程都是消费者线程)<"<<pthread_self()<<">解决的任务为: "<<task._x<<"+"<<task._y<<" = "<<task()<<endl;
        //注意和正常的生产者消费者模型不太一样的是:这里消费者处理完任务后不用pthread_cond_signal唤醒生产者,线程池的宗旨是有任务就处理,没任务就摸鱼,并不需要催促生产者
    }
}

template<class T>
class ThreadPool//用于处理T类型数据的线程池
{
    template<class K> friend void* routine(void* args);
public:
    ThreadPool(int num)//num表示线程池中需要多少线程
        :_num(num)
    {
        //是需要在构造函数中初始化锁的
        pthread_mutex_init(&_lock,nullptr);
        //是需要在构造函数中初始化条件变量的
        pthread_cond_init(&_cond,nullptr);
        for(int i=0;i<num;i++)
        {
            _v.push_back(new pthread_t);
        }

        for(pthread_t* x:_v)
        {
            //传this指针是因为在类外部的例行函数中需要访问类内的成员。
            pthread_create(x, nullptr, routine<T>, (void*)this);
        }
    }

    //该函数只被作为生产者的主线程调用
    void pushTask(const T& task)
    {
        pthread_mutex_lock(&_lock);
        _task_queue.push(task);
        pthread_mutex_unlock(&_lock);
        //生产者(即主线程)往交易场所中放置完任务后,需要唤醒某个消费者线程去接取任务
        pthread_cond_signal(&_cond);
    }

    ~ThreadPool()
    {
        for(vector<pthread_t*>::iterator it = _v.begin(); it != _v.end(); it++)
        {          
           //(*it)是vector中存的元素、注意每个元素只是线程ID的地址,而不是线程ID
            pthread_join(*(*it), nullptr);
            //join函数会释放绝大多数和线程相关的资源,但因为这里的线程ID对象是在构造函数中new出来的在堆上的变量,所以需要手动delete释放。
            delete (*it);
        }
        //是需要在析构函数中销毁锁的
        pthread_mutex_destroy(&_lock);
        //是需要在析构函数中销毁条件变量的
        pthread_cond_destroy(&_cond);
    }

private:
    vector<pthread_t*> _v;//线程池
    int _num;//统计_v中有多少个线程
    queue<T> _task_queue;//消费者(即线程池中的所有线程,注意线程池中的线程都是非主线程)都需要通过该任务队列获取任务;生产者(即主线程)需要往该任务队列中放置任务。所以本质上这个队列就是生产者消费者模型中的临界资源,形象点说叫交易场所
    pthread_mutex_t _lock;//分配给任务队列(即临界资源或者说交易场所)的锁,需要该锁的原因是需要维护【生产者和消费者的互斥关系】,避免发生【生产者放置任务的动作还只做到一半,消费者就跑进交易场所中接取任务】和【消费者接取任务的动作还只做到一半,生产者就跑进交易场所中放置任务】
    pthread_cond_t _cond;//分配给消费者(线程池中的所有线程都是消费者)的条件变量,当生产者(即主线程)不断往任务队列中push时,每push一个任务都应该试图唤醒一个线程,避免任务没有消费者接取从而全在任务队列中堆积。注意即使所有消费者线程都在工作,那我唤醒它也没关系,因为此时它会忽略这个唤醒信息,所以这里无脑唤醒即可。
};



Task.h文件的整体代码

首先创建一个Task.h文件,在里面实现一个Task类,这样一来,以后作为生产者的主线程的工作就是不断地创建Task类的对象。Task.h的整体代码如下。

#pragma once
#include<iostream>
using namespace std;
#include<functional>
 
typedef function<int(int,int)> func_t;//C++11的包装器
 
class Task
{
public:
    Task()
    {}
 
    ~Task()
    {}
   
    Task(int x,int y,func_t f):_x(x), _y(y), _f(f)
    {}
 
    int operator()()
    {
        return _f(_x,_y);
    }
    
    int _x;
    int _y;
    func_t _f;
};

然后要说的是,有了上文中实现的整个ThreadPool.h的代码后,模拟实现【基于线程池的生产者消费者模型】也就很简单了,思路为:

  • 首先创建一个ThreadPool<Task>线程池对象。因为其的底层实现,我们在构造出线程池对象后,所有消费者线程就已经创建完毕并开始运行了,但因为临界区即阻塞队列中并没有消费者关注的数据(或者说任务)资源,所以所有消费者线程就都被阻塞,正在等待生产者(即主线程)往阻塞队列中放置数据(或者说任务)。
  • 上文中也说过,所有线程中只有主线程作为唯一的生产者线程去生产任务,因为这里我们只是举例,所以就把生产任务的过程设计的简单点,就比如任务就是计算两个整形数据之和,那在主线程中,我们只需要通过rand函数生成两个随机数,然后用户随便编写一个逻辑为加法的可调用对象,然后把这些内容作为参数传给Task对象以让Task对象完成初始化,到这里生产者就完成了一个任务的生产,然后生产者就需要把该任务放置到阻塞队列中并唤醒某个消费者线程。这样一来,某个消费者线程就可以获取任务并处理任务了。

testMain.cc文件的整体代码

结合上面的思路,以下是整个testMain.cc的代码。(注意g++编译C++文件时,其后缀名一定得是.cc)

#include"ThreadPool.h"
#include"Task.h"

int main()
{
    srand(time(0));
    ThreadPool<Task> p(3);
    while(1)
    {
        int x = rand()%100;
        int y = rand()%100;
        Task t(x, y, [](int x, int y)->int{return x+y;});
        cout<<"生产者(即主线程)生产出的任务为"<< x << '+' << y << " = ?" << endl;
        //每0.7s生产一个任务,防止生产和消费的速度都太快而造成打印语句刷屏
        usleep(7000);
        p.pushTask(t);
    }

    return 0;
}

对【基于线程池的生产者消费者模型的模拟实现】的测试

把上面的testMain.cc文件进行编译运行后,结果如下,可以看到是有不同的消费者线程在处理任务的,符合我们的预期。

注意之所以下面是【生产者线程每生产一个任务,消费者线程就能立刻处理一个任务,双方的步调一致】而不是【生产者连续生产一批任务后,消费者再连续消费一批任务,双方的步调不一致】是因为我们在生产者的线程函数(即main函数)中让生产者每生产出一个任务后就usleep阻塞0.7秒。因为我们没有sleep限制消费者线程消费的速度,所以消费者消费的速度就比生产者生产的速度快,但即使消费者消费的速度比生产者生产的速度快,因为不生产是无法消费的,所以双方的步调只能是一致的,所以打印出的结果是步调一致的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 下面是一个使用 Qt 线程池实现生产者消费者模型的简单示例: ```cpp #include <QtCore> #include <iostream> class Producer : public QObject { Q_OBJECT public: Producer(QThreadPool *pool, QMutex *mutex, QWaitCondition *cond, QList<int> *data) : m_pool(pool), m_mutex(mutex), m_cond(cond), m_data(data) { m_count = 0; } public slots: void produce() { int num = qrand() % 100; QRunnable *task = new Task(num, m_mutex, m_cond, m_data); m_pool->start(task); m_count++; std::cout << "Produced " << num << std::endl; m_cond->wakeAll(); } int count() const { return m_count; } private: QThreadPool *m_pool; QMutex *m_mutex; QWaitCondition *m_cond; QList<int> *m_data; int m_count; }; class Consumer : public QObject { Q_OBJECT public: Consumer(QMutex *mutex, QWaitCondition *cond, QList<int> *data) : m_mutex(mutex), m_cond(cond), m_data(data) { } public slots: void consume() { m_mutex->lock(); while (m_data->isEmpty()) { m_cond->wait(m_mutex); } int num = m_data->takeFirst(); std::cout << "Consumed " << num << std::endl; m_mutex->unlock(); } private: QMutex *m_mutex; QWaitCondition *m_cond; QList<int> *m_data; }; class Task : public QRunnable { public: Task(int num, QMutex *mutex, QWaitCondition *cond, QList<int> *data) : m_num(num), m_mutex(mutex), m_cond(cond), m_data(data) { } void run() { m_mutex->lock(); while (m_data->size() >= 10) { m_cond->wait(m_mutex); } m_data->append(m_num); m_mutex->unlock(); } private: int m_num; QMutex *m_mutex; QWaitCondition *m_cond; QList<int> *m_data; }; int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); qsrand(QTime::currentTime().msec()); QThreadPool pool; pool.setMaxThreadCount(5); QMutex mutex; QWaitCondition cond; QList<int> data; Producer producer(&pool, &mutex, &cond, &data); Consumer consumer(&mutex, &cond, &data); QObject::connect(&producer, &Producer::produce, &consumer, &Consumer::consume); QTimer timer; QObject::connect(&timer, &QTimer::timeout, &producer, &Producer::produce); timer.start(1000); QTimer countTimer; QObject::connect(&countTimer, &QTimer::timeout, [&producer]() { std::cout << "Produced " << producer.count() << " items" << std::endl; }); countTimer.start(5000); return app.exec(); } ``` 在上面的示例中,`Producer` 和 `Consumer` 类继承自 `QObject`,并使用 Qt 信号和槽机制进行通信。`Producer` 类的 `produce()` ### 回答2: Qt线程池可以很好地实现生产者消费者模型生产者消费者模型主要是通过一个共享的缓冲区来实现生产者生产数据,消费者消费数据的过程。下面是一个使用Qt线程池实现生产者消费者模型的示例: 1. 定义一个数据结构,用于存储生产者消费者共享的数据。这个数据结构可以是一个队列或者一个列表。 2. 创建一个Qt线程池,并设置线程池的最大线程数。 3. 创建生产者线程和消费者线程,并将它们添加到线程池中。 4. 在生产者线程中,生产数据并将数据添加到共享的数据结构中。可以使用Qt的信号槽机制,在生产者线程中发送信号,将数据发送给消费者线程。 5. 在消费者线程中,从共享的数据结构中获取数据并进行消费。可以使用Qt的信号槽机制,在消费者线程中接收信号,并处理收到的数据。 6. 在主线程中,等待生产者线程和消费者线程完成工作,并关闭线程池。 通过使用Qt线程池,可以方便地管理多个线程,避免手动管理线程的创建和销毁,从而简化了生产者消费者模型实现。此外,Qt的信号槽机制可以方便地实现线程间的通信,从而实现生产者消费者之间的数据传递。 ### 回答3: Qt是一个跨平台的C++应用程序开发框架,它提供了丰富的类库和工具,其中也包括线程池实现。在Qt中,可以使用QThreadPool类来创建和管理线程池生产者消费者模型是一种常见的多线程编程模型,其中有一组线程作为生产者,负责生成数据,另一组线程作为消费者,负责处理这些数据。线程池可以很好地支持这种模型,并提供了以下步骤来实现: 1. 创建一个线程池对象:使用QThreadPool类创建一个线程池对象,设置最大线程数、线程闲置时间等属性。 2. 创建生产者线程:通过继承QRunnable类,实现自己的生产者线程类。在类中重写run()函数,在其中完成需要生产的数据的生成。 3. 创建消费者线程:同样通过继承QRunnable类,实现自己的消费者线程类。在类中重写run()函数,在其中完成对生产者生成的数据的处理。 4. 将任务添加到线程池:使用QThreadPool的start()函数将生产者消费者线程对象添加到线程池中,线程池会自动分配线程去运行这些任务。 5. 等待线程池完成任务:可以使用QThreadPool的waitForDone()函数来等待线程池中的所有任务完成,确保所有生产者消费者线程都执行完毕。 通过以上步骤,我们可以在Qt中实现简单的生产者消费者模型线程池可以很好地管理线程的创建和销毁,提高线程利用率和系统的性能。同时,Qt的线程池也提供了一些其他的功能,比如任务优先级和取消线程等,可以根据实际需求来灵活调整。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值