Linux--线程池

发布于:2025-07-03 ⋅ 阅读:(15) ⋅ 点赞:(0)

目录

Linux线程池

线程池的概念

线程池的优点

线程池的应用场景

线程池的实现


Linux线程池

线程池的概念

线程池是一种线程的使用模式。

其存在的主要原因就为:线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。

线程池的优点

  1. 线程池避免了再处理短时间任务时创建于销毁线程的代价。
  2. 线程池不仅能够保证内核的充分利用,还能防止过分调度。 

注意:线程池可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

线程池的应用场景

线程池常见的应用场景如下:

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。
  2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。

相关现实场景应用:

  1. WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。
  2. 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
  3. 突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.

线程池的实现

下面我们用图示,表达一个简单的线程池,线程池中提供了一个任务队列,以及若干个线程(多线程)。

  • 线程池中的多个线程负责从任务队列当中拿任务,并将拿到的任务进行处理。
  • 线程池对外提供一个Push接口,用于让外部线程能够将任务Push到任务队列当中。

线程池的代码如下:

.hpp  (ThreadPool.hpp)

#include <iostream>
#include <unistd.h>
#include <queue>
#include <stdlib.h>
#include <pthread.h>

class ThreadInfo
{
public:
    pthread_t tid_;
    std::string name_;
};

static const int defalutnum = 5;

template <class T>
class ThreadPool
{
public:
    void Lock()
    {
        pthread_mutex_lock(&mutex_);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&mutex_);
    }
    void Wakeup()
    {
        pthread_cond_signal(&cond_);
    }
    void ThreadSleep()
    {
        pthread_cond_wait(&cond_, &mutex_);
    }
    bool IsQueueEmpty()
    {
        return tasks_.empty();
    }
    std::string GetThreadName(pthread_t tid)
    {
        for(const auto& ti:threads_)
        {
            if(ti.tid_ == tid)
            {
                return ti.name_;
            }
        }
        return "None";
    }
public:
    static void *HandlerTask(void *args) // 带 static 是因为类内的函数第一个参数是默认为 this 指针
    {
        ThreadPool<T> *tp = static_cast<ThreadPool*>(args);
        std::string name = tp->GetThreadName(pthread_self());

        while(true)
        {
            tp->Lock();

            while(tp->IsQueueEmpty())
            {
                tp->ThreadSleep();
            }
            // 分配到任务
            T t = tp->Pop();
            std::cout<< 分配到了一个任务 : " << t << "," << name << "run" << std::endl;
            tp->Unlock();

            sleep(1);
            // 后续的任务处理部分
            // run();
        }
    }
    void Start()
    {
        int num = defalutnum;
        for(int i=0; i < num; i++)
        {
            threads_[i].name_ = "thread-" + std::to_string(i + 1);
            pthread_create(&(threads_[i].tid_), nullptr, HandlerTask, this);
        }
    }
    T Pop()
    {
        T t = tasks_.front();
        tasks_.pop();
        return t;
    }
    void Push(const T &t)
    {
        Lock();
        tasks_.push(t);
        Wakeup();
        Unlock();
    }
public:
    ThreadPool(int num = defalutnum) :threads_(num)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
private:
    std::vector<ThreadInfo> threads_;
    std::queue<T> tasks_;
    
    pthread_mutex_t mutex_;
    pthread_cond_t cond_;
};

知识回顾:为什么线程池中需要有互斥锁和条件变量?

因为线程池中的任务队列是会被多个执行流同时访问的临界资源,因此为了避免数据二义性,就会创建互斥锁与条件变量来进行保护。

线程池当中的线程要从任务队列里拿任务,前提条件是任务队列中必须要有任务,因此线程池当中的线程在拿任务之前,需要先判断任务队列当中是否有任务,若此时任务队列为空,那么该线程应该进行等待,直到任务队列中有任务时再将其唤醒,因此我们需要引入条件变量。

当外部线程向任务队列中Push一个任务后,此时可能有线程正处于等待状态,因此在新增任务后需要唤醒在条件变量下等待的线程。

注意:

  • 当某线程被唤醒时,其可能是被异常或是伪唤醒,或者是一些广播类的唤醒线程操作而导致所有线程被唤醒,使得在被唤醒的若干线程中,只有个别线程能拿到任务。此时应该让被唤醒的线程再次判断是否满足被唤醒条件,所以在判断任务队列是否为空时,应该使用while进行判断,而不是if。
  • 这里的唤醒是用的pthread_cond_signal而不是pthread_cond_broadcast,那是因为,如果外部向队列中push了一个任务,但我们却使用pthread_cond_broadcast,将所有的等待线程全部唤醒,但实际情况却仅需要一个线程,那这肯定会资源的浪费。一瞬间唤醒大量的线程可能会导致系统震荡,这叫做惊群效应。所以在唤醒线程时最好还是使用pthread_cond_signal函数唤醒一个正在等待的线程即可。
  • 当线程从任务队列中拿到任务后,该任务就已经属于当前线程了,与其他线程已经没有关系了。因此应该在解锁之后再进行处理任务,而不是在解锁之前进行。因为处理任务的过程可能会耗费一定的时间,所以我们不要将其放到临界区当中。
  • 还有就是设计的效率问题,如果我们将处理任务的操作也放在临界区来处理,那么当某一线程从任务队列中拿到任务后,其他线程还需要等待该线程将任务处理完后,才有机会进入临界区。这样的设计虽然也叫做线程池,但显然效率是远没有仅仅将拿取任务部分放在临界区的效率高。

 为什么线程池中的线程执行例程需要设置为静态方法?

使用pthread_create函数创建线程时,需要为创建的线程传入一个HandlerTask(执行例程),该HandlerTask只有一个参数类型为void*的参数,以及返回类型为void*的返回值。

而此时HandlerTask作为类的成员函数,该函数的第一个参数是隐藏的this指针,因此这里的HandlerTask函数,虽然看起来只有一个参数,而实际上它有两个参数,此时直接将该HandlerTask函数作为创建线程时的执行例程是不行的,无法通过编译。

静态成员函数属于类,而不属于某个对象,也就是说静态成员函数是没有隐藏的this指针的,因此我们需要将HandlerTask设置为静态方法,此时HandlerTask函数才真正只有一个参数类型为void*的参数。

但是在静态成员函数内部无法调用非静态成员函数,而我们需要在HandlerTask函数当中调用该类的某些非静态成员函数,比如Pop。因此我们需要在创建线程时,向Routine函数传入的当前对象的this指针,此时我们就能够通过该this指针在HandlerTask函数内部调用非静态成员函数了。

 主函数:.cpp  (proc.cc)

#include <iostream>
#include <ctime>
#include "ThreadPool.hpp"

int main()
{
    std::cout << "process running..." << std::endl;
    sleep(3);
    ThreadPool<int> *tp = new ThreadPool<int>();
    tp->Start();
    srand(time(nullptr) ^ getpid());

    while(true)
    {
        //1. 构建任务
        int x = rand() % 10 + 1;

        tp->Push(x);
        //2. 交给线程池处理
        std::cout << "proc thread make task: " << std::endl;

        sleep(1);
    }
}

 makefile

Threadpool:proc.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f Threadpool

 运行效果如下:


网站公告

今日签到

点亮在社区的每一天
去签到