C++模拟实现线程池模型

线程池是一种多线程的处理模式,在处理的过程中将任务添加到队列中,在创建线程后启动这些任务。
我们知道,生产一个线程的开销比一个进程要小的多,线程的销毁也比进程方便。但是我们在调用多线程的时候,不同线程之间抢占式执行CPU资源的时候,会有一个时间片的问题。

当一个线程在CPU时间片到了的时候,就需要让出CPU资源,这时会通过共享区中的程序计数器和上下文信息来保存当前的状态,等到下次执行的时候再恢复过来。如果涉及的线程太多,就会造成线程之间频繁的调用程序计数器和上下文信息来恢复执行状态,就是造成时间上的浪费。

应用场景

  • 需要大量的线程来完成任务,并且完成任务的时间比较短
  • 对性能要求严格的应用
  • 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用

示例

线程池 = 线程安全队列 + 线程(消费线程)

  • 创建固定数量的线程池,然后循环从任务队列中获取任务对象
  • 获取任务对象后,然后执行任务对象中的任务接口

在这里插入图片描述
线程池中的每一个线程都是一种角色线程,也就是每个线程所执行入口函数都是相同的。
为了满足不同的函数需求,可以使用switch 或者 函数指针来操作。

  • switch:switch语句处理一个或者几个还比较可以,但是一般情况都是很多种的,这时用switch就比较麻烦
  • 向线程池中的队列中插入数据的时候,把对这个数据的处理函数也一起插入进去(函数指针,函数名代表着这个函数的地址),然后线程池中的线程处理队列中的数据时就可以利用他自带的函数来操作
  • 线程队列中需要的元素:数据 + 处理函数的地址

程序实现

分析

  1. 要有一个线程安全的队列
  2. 为了保证队列安全,需要使用同步和互斥
  3. 创建一些线程来执行数据处理函数

线程任务类

  • 类的私有成员变量为:需要储存到队列中的数据 + 数据的执行函数
  • 该类的自定义类型就是队列中元素的类型
typedef void* (*Execute_Fun)(int);

//queue data 
class ThreadTask
{
public:
  ThreadTask(int data,Execute_Fun Function)
    :_data(data),
    _Function(Function)
  {}

  //处理数据
  void Run()
  {
    sleep(1);//执行任务耗时1秒
    _Function(_data);
  }
private:
  int _data;
  Execute_Fun _Function;
}

Execute_Fun就是冲定义的一个函数指针 typedef void* (*Execute_Fun)(int);

安全队列 --> 线程池

构造函数以及成员变量

  • 构造函数中需要完成对队列容量,线程的最大容量,创建线程的数量,线程当前的状态(是否可以退出)的进行初始化工作
  • 对互斥锁和同步的条件变量进行初始化
  • 创建指定数量的线程
//threadpool
class ThreadPool
{
public:
  ThreadPool(int capacity = CAPACITY,int thread_capacity = THREADCOUNT)
    :_capacity(capacity),
    _thread_capacity(thread_capacity)
  {
    pthread_mutex_init(&_lock,NULL);
    pthread_cond_init(&_cond,NULL);
    
    //创建线程
    bool isCreate = ThreadCreate();
    if(!isCreate)
    {
      cout<<"threadpool create thread error"<<endl;
      _exit(1);
    }
    
    //创建线程成功,初始化状态为未离开,满容量
    _isExit = false;
    _thread_count = _thread_capacity;
  }
  
private:
  queue<ThreadTask*> _que;
  size_t _capacity;
  //互斥
  pthread_mutex_t _lock;
  //同步
  //仅仅是生产者的同步,消费者的功能在自己函数中自带
  pthread_cond_t _cond;

  size_t _thread_capacity;//线程池最大容量
  size_t _thread_count; //当前存在的线程数量
  size_t _tid[THREADCOUNT]; //线程标识符
  bool _isExit; //是否可以退出
};

安全push以及pop

push:

  • push中是对临界资源的访问,入队的时候需要满足互斥条件,在入队前后进行加锁解锁操作
  • 如果在资源push之前,队列中线程的状态为可以退出,就需要进行解锁,并停止该资源的入队操作,返回false。需要满足线程对资源的合理访问
  • 成功入队并解锁之后,就可以通知线程池中的线程去调用队列中数据的处理函数去处理数据了

pop:

  • 因为pop我们需要获取队列中的队头元素,我们需要传入的是ThreadTask*类型的地址。所以说实参的类型就是ThreadTask**类型
  • 我们pop的过程是在线程的执行任务中进行的,线程在执行函数时就已经满足了互斥条件,所以pop这里不需要再加锁。
  bool push(ThreadTask* tt)
  {
    pthread_mutex_lock(&_lock);
    if(_isExit)
    {
      pthread_mutex_lock(&_lock);
      return false;
    }
    _que.push(tt);
    pthread_mutex_unlock(&_lock);

    //通知线程池中的线程调用消费者函数
    pthread_cond_signal(&_cond);
    return true;
  }

  bool pop(ThreadTask** tt)
  {
    *tt = _que.front();
    _que.pop();
    return true;
  }

私有成员函数

线程执行函数

  • 这里因为线程入口函数只有一个参数,但是我们定义在类中的时候,类默认有一个this指针,这就占据了一个参数位置。为了消除this指针,我们可以采用static静态函数,或者友元。
  • 线程的任务就是执行pop出来的临界资源,所以在线程操作的前后就需要满足互斥条件
  • 因为我们的临界资源都是new来开辟出来的,而临界资源的释放得要等到这个资源被执行完毕的时候,所以正好就可以放在线程Run函数的后面,在释放就可以保证安全释放。
  • 如果队列为空的时候,我们的线程就需要进行等待

线程创建函数

  • 成功创建返回true,失败返回false。
  • pthread_create函数中线程执行函数的参数需要传的是*this,这个就是线程入口函数的参数,不能写NULL
private:
  //使用static 取消类中自带的this指针的干扰
  //调用函数只有一个参数,或者使用友元
  static void* ThreadStart(void* arg)
  {
    ThreadPool* tp = (ThreadPool*)arg;
    
    while(1)
    {
      //从队列中取资源必须是互斥的
      pthread_mutex_lock(&(tp->_lock));
      ThreadTask* tt;

      while(tp->_que.empty())
      {
        if(tp->_isExit)
        {
          //退出 
          tp->_thread_count--;
          pthread_mutex_unlock(&(tp->_lock));
          pthread_exit(NULL);
        }

        pthread_cond_wait(&(tp->_cond),&(tp->_lock));
      }
      tp->pop(&tt);
      pthread_mutex_unlock(&(tp->_lock));

      //取到资源,调用处理数据的函数去执行该资源的操作
      tt->Run();
      //防止内存泄漏
      delete tt;
    }
  }

  bool ThreadCreate()
  {
    for(size_t i = 0; i < _thread_capacity; i++)
    {
      int ret = pthread_create(&_tid[i],NULL,ThreadStart,(void*)this);
      if(ret < 0)
      {
        perror("pthread_create");
        return false;
      }
    }
    return true;
  }

如何让线程池中的线程安全合理的退出

原因:防止线程在退出的时候,线程池的安全队列中还有数据没有处理
在这里插入图片描述
在这里插入图片描述

  • 位置1,加互斥锁的地方。
    程序流程:加锁 -》判断队列是否为空 -》判断线程退出条件 _isExit -》退出
  • 位置2,调用pthread_cond_wait当中
    程序流程:需要退出的线程会阻塞在这个接口等待函数中
  • 位置3,获取队列中的数据中
    程序流程:获取成功 -》处理数据 -》while循环 -》加锁 -》判断队列是否为空 -》 判断线程退出条件 _isExit -》退出
  • 位置4,执行处理数据的函数时
    程序流程:处理数据 -》while循环 -》加锁 -》判断队列是否为空 -》 判断线程退出条件 _isExit -》退出

可以看出,四种情况下,需要退出的线程都会阻塞在等待接口中,所以在阻塞接口前判断线程是否需要退出刚合适。

线程等待与线程退出函数

线程退出

  • 在线程的退出函数中,只要有线程退出,就通知PCB等待队列中的所有线程,让他们取消等待,不能让PCB等待队列中的线程一直等待
  • 在修改_isExit变量的时候,这也是一个临界资源,也是需要加锁与解锁的
  //线程等待
  void ThreadJoin()
  {
    for(size_t i = 0; i < _thread_count; i++)
    {
      pthread_join(_tid[i],NULL); 
    }
  }
  //线程退出时,清空线程池
  void ThreadPoolClear()
  {
    pthread_mutex_lock(&_lock);
    _isExit = true;
    pthread_mutex_unlock(&_lock);

    if(_thread_count > 0)
    {
      //移出PCB等待队列中的所有线程,让他们取消等待,执行退出函数
      pthread_cond_broadcast(&_cond);
    }
  }

主函数

void* DealData(int data)
{
  cout<<"consum data is [ "<<data<<" ]"<<endl;
  return NULL;
}

int main()
{
  ThreadPool* tp = new ThreadPool();

  for(int i = 1; i <= 50; i++)
  {
    ThreadTask* tt = new ThreadTask(i,DealData);
    tp->push(tt);
  }

  //等待线程任务执行完毕 
  sleep(20);//20秒一定执行完了 
  tp->ThreadPoolClear();
  tp->ThreadJoin();
  delete tp;

  return 0;
}

运行结果

线程任务执行时(再次验证抢占式执行)
在这里插入图片描述
20秒后调用清理函数,使线程安全退出
在这里插入图片描述

源码地址

https://github.com/duchenlong/linux-text/tree/master/thread/threadpool

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值