【Linux】编写一个简单的线程池


一、线程池的作用

申请内存是有成本的。每次申请都涉及用户到内核,内核到用户的身份切换,每次申请都需要用OS申请内存的算法。这样就会比较低效。线程池就可以减少了若干次用户到内核的申请!

线程池原理:
即一次创建一大批线程,让这些线程处于待机状态。

线程池注意的细节:
处理任务的过程不应该在锁当中,因为任务已经从queue这个临界区当中拿走了,已经放到了线程私有的栈当中,此时可以将锁释放,给其他线程进行取任务。

线程池的意义在于能够有大量的线程在其中作为消费者的角色,当我们塞进去大量任务的时候,线程池内部可以同步的把任务进行执行。

所以线程池中每个线程都可以看做消费者,而将任务放进去的可以认为是生产者。

ThreadPool.hpp

#pragma once 
#include<iostream>
#include<queue>
#include<unistd.h>
#include<pthread.h>
template<class T>
class ThreadPool
{
    
    
  private:
    int num;
    std::queue<T> q;
    pthread_mutex_t _mtx;
    pthread_cond_t _cond;//条件变量,这里是基于阻塞队列
  public:
    ThreadPool(int n):num(n)
   {
    
    
    pthread_mutex_init(&_mtx,nullptr);
    pthread_cond_init(&_cond,nullptr);
   }
    ~ThreadPool()
    {
    
    
      pthread_mutex_destroy(&_mtx);
      pthread_cond_destroy(&_cond);
    }

    void PutTask(const T& in)
    {
    
    
      //以防外部多线程放任务
      Lock();
      q.push(in);
      pthread_cond_signal(&_cond);
      UnLock();
    }

    void PopTask(T* out)
    {
    
    
      //已经在临界区才会取出任务了
      *out = q.back();
      q.pop();
    }
    void Lock()
    {
    
    
        //std::cout << "4"<<std::endl;
        pthread_mutex_lock(&_mtx);
        //std::cout << "5"<<std::endl;
    }

    void UnLock()
    {
    
    
        pthread_mutex_unlock(&_mtx);
    }
    void ThreadSleep()
    {
    
    
      pthread_cond_wait(&_cond,&_mtx);
    }
    void ThreadWait()
    {
    
    
      pthread_cond_signal(&_cond);
    }
    bool IsEmpty()
    {
    
    
      return q.size() == 0;
    }
    //只能访问其他的static属性的成员方法和变量
    static void* Routine(void* args)
    {
    
    
      ThreadPool<T>* tp = (ThreadPool<T>*) args;
      while(1)
      {
    
    
        tp->Lock();
        //std::cout << "3"<<std::endl;
        //检测是否有任务,取任务,执行任务
        while(tp->IsEmpty())
        {
    
    
        //std::cout << "2"<<std::endl;
          tp->ThreadSleep();
        }
        //std::cout << "1"<<std::endl;
        T t; 
        tp->PopTask(&t);
        tp->UnLock();
        //处理逻辑
        t();
      }
      return nullptr;
    }
    void InitThreadPool()
    {
    
    
        for(int i = 0; i < num;++i)
        {
    
    
          pthread_t tid;
          //这里的Routine会多传一个this指针
          pthread_create(&tid,nullptr,Routine,this);
          pthread_detach(tid); 
        }
    }


};

test.cc


#include<iostream>
using namespace std;
#include"Task.hpp"
#include<cstdlib>
#define NUM 5
void Productor(ThreadPool<Task>* tp)
{
    
    
  srand((unsigned)0);
  sleep(3);
  while(1)
  {
    
    
    int x = rand()%100;
    Task t(x);
    sleep(1);
    cout << "consume put task " << x <<endl;
    tp->PutTask(t);
  }
}
int main()
{
    
    
  ThreadPool<Task>* tp = new ThreadPool<Task>(NUM);
  tp->InitThreadPool();
  Productor(tp);
  delete tp;

  return 0;
}

Task.hpp

#pragma once 
#include<iostream>
#include<pthread.h>

//假设Task的逻辑就是将一个数字的所有位数加起来返回
class Task 
{
    
    
  private:
    int _n;
  public:

    void operator()()
    {
    
    
      run();
    }
    Task(int n):_n(n)
    {
    
    }
    Task()
    {
    
    }
    ~Task()
    {
    
    }

    void run()
    {
    
    
      int i = _n;
      int res = 0;
      while(i)
      {
    
    
        res += i%10;
        i/=10;
      }
      std::cout <<"["<<pthread_self() <<"] "<< _n <<":"<< res<< std::endl;

    }
};

结果:因为运行Task任务的都是线程池当中的线程,所以在Task.hpp我们打印出他们的pthread id,可以发现是同步运行的。

在这里插入图片描述

细节:

其中一定要注意由于线程是在类内部创建的,线程调用函数的时候会多传一个对象的指针,可是Routine方法只允许传一个参数,所以这个时候我们这能将Routine方法设置为static,调用static函数的时候是不会传递this指针的,但是这样也有坏处,static函数只能访问static的成员和函数,所以这个时候需要对上锁,解锁,向阻塞队列插入节点,释放节点都需要写成static函数,所以我们选择Routine传递参数的时候传递一个this指针,这样this指针调用函数就可以不需要将所有的函数都设置为static,只需要封装一层即可。

二、设置线程池为单例模式

ThreadPool.hpp

#pragma once 
#include<iostream>
#include<queue>
#include<unistd.h>
#include<pthread.h>
template<class T>
class ThreadPool
{
    
    
  private:
    int num;
    std::queue<T> q;
    pthread_mutex_t _mtx;
    pthread_cond_t _cond;//条件变量,这里是基于阻塞队列
    static pthread_mutex_t _lock;
  private:
    static ThreadPool* tp;
      ThreadPool(int n):num(n)
	   {
    
    
	    pthread_mutex_init(&_mtx,nullptr);
	    pthread_cond_init(&_cond,nullptr);
	   }
  public:
    static ThreadPool* getinstance()
    {
    
    
      if(tp == nullptr)
      {
    
    
        //锁可以定义在这里,定义成静态的,也可以定义成成员变量,我们用的是成员变量。
        pthread_mutex_lock(&_lock);
        if(tp == nullptr)
        {
    
    
          tp = new ThreadPool<T>(5);
        }
        pthread_mutex_unlock(&_lock);
      }
      return tp;
    }
  public:
  	//讲拷贝构造和赋值重载删除
  	ThreadPool(const ThreadPool& tp) = delete;
  	ThreadPool& operator=(const ThreadPool& tp) = delete ;
  	
  
    ~ThreadPool()
    {
    
    
      pthread_mutex_destroy(&_mtx);
      pthread_cond_destroy(&_cond);
    }

    void PutTask(const T& in)
    {
    
    
      //以防外部多线程放任务
      Lock();
      q.push(in);
      pthread_cond_signal(&_cond);
      UnLock();
    }

    void PopTask(T* out)
    {
    
    
      //已经在临界区才会取出任务了
      *out = q.back();
      q.pop();
    }
    void Lock()
    {
    
    
        pthread_mutex_lock(&_mtx);
    }

    void UnLock()
    {
    
    
        pthread_mutex_unlock(&_mtx);
    }
    void ThreadSleep()
    {
    
    
      pthread_cond_wait(&_cond,&_mtx);
    }
    void ThreadWait()
    {
    
    
      pthread_cond_signal(&_cond);
    }
    bool IsEmpty()
    {
    
    
      return q.size() == 0;
    }
    //只能访问其他的static属性的成员方法和变量
    static void* Routine(void* args)
    {
    
    
      ThreadPool<T>* tp = (ThreadPool<T>*) args;
      while(1)
      {
    
    
        tp->Lock();
        //检测是否有任务,取任务,执行任务
        while(tp->IsEmpty())
        {
    
    
          tp->ThreadSleep();
        }
        T t; 
        tp->PopTask(&t);
        tp->UnLock();
        //处理逻辑
        t();
      }
      return nullptr;
    }
    void InitThreadPool()
    {
    
    
        for(int i = 0; i < num;++i)
        {
    
    
          pthread_t tid;
          //这里的Routine会多传一个this指针
          pthread_create(&tid,nullptr,Routine,this);
          pthread_detach(tid); 
        }
    }


};

main.cc 调用函数的方式发生变化,注意要在.cc对.hpp中的类中static变量进行定义。

#include"ThreadPool.hpp"

#include<iostream>
using namespace std;
#include"Task.hpp"
#include<cstdlib>
#define NUM 5

template<class T>
pthread_mutex_t ThreadPool<T>::_lock = PTHREAD_MUTEX_INITIALIZER;

template<class T>
ThreadPool<T>* ThreadPool<T>::tp = nullptr;
void Productor(ThreadPool<Task>* tp)
{
    
    
  srand((unsigned)0);
  sleep(3);
  while(1)
  {
    
    
    int x = rand()%100;
    Task t(x);
    sleep(1);
    cout << "consume put task " << x <<endl;
    tp->PutTask(t);
  }
}
int main()
{
    
    
  ThreadPool<Task>* tp = ThreadPool<Task>::getinstance();

  tp->InitThreadPool();
  Productor(tp);


  return 0;
}

三、锁


  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS(Java)操作。通过版本来区分每个人的修改。

我们大多数用的是悲观锁。

自旋锁


之前的悲观锁是当申请不到时,立马被挂起。

自旋锁就是有一种场景,当我们知道临界区的线程可能并不需要运行很久就会出来的时候,我们线程可能在锁下面还没开始等就要被唤醒了,此时是不太好的,这个时候我们选择轮询检测(检测锁是否已经就绪),这个过程可以称之为自旋检测。 的方式,让线程不被挂起。也就是当等待的时间太短暂了,此时如果线程频繁挂起和唤醒会造成效率的损失。使用什么锁取决于拥有锁的线程在临界区中执行的时长。

写磁盘,链接数据库应该用悲观锁,而一些简单计算应当用自旋锁。

接口与之前的类似:
pthread_spin_init,pthread_spin_destroy
pthread_spin_lock,pthread_spin_trylock
pthread_spin_unlock

自旋锁的伪代码

while(1)
{
    
    
	if(check_lock())
	{
    
    
		break;
	}
}

读者写者模型

应用场景:读的人多,次数频繁,写的人少!

三种关系:
读者和读者,写者和写者,读者和读者。

写者和写者:互斥
读者和读者:没有影响,因为读者并不会对数据进行修改,也就不会影响其他读者。
读者和写者:互斥关系,当没有写完读可能会解析错误。同步关系(避免读饥饿或者写饥饿)。

两种角色:读者和写者

一个交易场所:一段缓冲区

接口:
pthread_rwlock_init 创建读写锁
pthread_rwlock_rdlock 读者加锁
pthread_rwlock_wrlock 写者加锁
释放锁:pthread_rwlock_unlock

由于写者和其他的关系都为互斥,所以实现比较简单。

读者伪代码:

int reader_cnt = 0;
lock();
reader_cnt ++;
unlock();

//....进行读操作,访问临界资源

lock();
reader_cnt--;
unlock();

写者伪代码:当临界区有读者,就不会对数据进行修改。

start:
lock();
if(reader_cnt > 0)
{
    
    
	//有读者,无法进行写操作
	unlock();
	goto start;
}
//走到这里,没有读者,并且占有锁
//访问临界资源.....

生产者消费者低位对等,互相依赖,而读写者并没有太大的依赖关系。
为了防止读饥饿或写饥饿,有两种策略:
读者优先,写者优先:采用哪种看业务,若是新闻媒体则写优先;

读优先:读者和写者一块来,优先让读者进入临界区。

写者优先:当写者到来的时候,后续的读者不能进入临界资源进行读取了,所有的正在读取的线程执行完毕,写者再进入。 所以写优先也必须要在所有进入临界区的线程读取完毕。


!!!_!!! 可以尝试将上述两种锁都用作线程池当中。

猜你喜欢

转载自blog.csdn.net/weixin_52344401/article/details/123781281