[Linux]线程池

[Linux]线程池

文章目录

线程池的概念

线程池是一种线程使用模式。线程池是一种特殊的生产消费模型,用户作为生产者,线程池作为消费者和缓冲区。

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

线程池的优点

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

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

线程池的应用场景

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

线程池的实现

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

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

线程池的代码如下:

cpp 复制代码
#pragma once

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

const int N = 5; // 线程池内线程数量

template <class T>
class ThreadPool
{
public:
    ThreadPool(int num = N) : _num(num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }

    void LockQueue()
    {
        pthread_mutex_lock(&_mutex);
    }

    void UnLockQueue()
    {
        pthread_mutex_unlock(&_mutex);
    }

    void threadWait()
    {
        pthread_cond_wait(&_cond, &_mutex);
    }

    void threadWakeUP()
    {
        pthread_cond_signal(&_cond);
    }

    T getTask()
    {
        T t = _tasks.front();
        _tasks.pop();
        return t;
    }

    bool isEmpty()
    {
        return _tasks.empty();
    }

    static void *threadRoutine(void *args)
    {
        pthread_detach(pthread_self());

        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        while (true)
        {
            tp->LockQueue();
            while (tp->isEmpty())
            {
                tp->threadWait();
            }
            T t = tp->getTask();
            tp->UnLockQueue();
            t.Run();//任务处理
        }
    }

    void Start()
    {
        pthread_t tid;
        for (int i = 0; i < _num; i++)
        {
            pthread_create(&tid, nullptr, threadRoutine, this);
        }
    }

    void PushTask(T &task) // 添加任务
    {
        LockQueue();
        _tasks.push(task);
        threadWakeUP();
        UnLockQueue();
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

private:
    int _num;                        // 线程数
    std::queue<T> _tasks;            // 任务队列

    pthread_mutex_t _mutex; // 保证互斥访问任务队列这一共享资源
    pthread_cond_t _cond;   // 根据任务队列中的任务数量控制线程的等待和运行
};

为什么线程池中需要有互斥锁和条件变量?

互斥锁: 任务队列是一个共享资源,外部线程可以调用添加任务的接口访问任务队列,线程池内部的线程可以直接访问任务队列处理任务,可能会造成任务队列的并发访问问题,因此需要利用互斥锁保护任务队列中的数据。

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

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

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

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

如果threadRoutine作为类的成员函数,该函数的第一个参数是隐藏的this指针,无法通过编译。而静态成员函数属于类,而不属于某个对象,也就是说静态成员函数是没有隐藏的this指针的,因此我们需要将threadRoutine设置为静态方法,此时threadRoutine函数才真正只有一个参数类型为void的参数。

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

任务类型的设计

由于线程池编写的是模板化的,因此任务类型可以是任意的,但是由于处理任务的逻辑是通过调用任务的Run函数,因此任务类中必须实现Run函数才能使用该线程池。

例如,实现一个计算任务类如下:

cpp 复制代码
#include <cstdlib>
#include <iostream>

class Task
{
public:
    Task(int x, int y, char op) : _x(x), _y(y), _op(op), _result(0), _exitcode(0)
    {}

    void Run()//对传入数据进行操作
    {
        switch (_op)
        {
        case '+':
            _result = _x + _y;
            break;
        case '-':
            _result = _x - _y;
            break;
        case '*':
            _result = _x * _y;
            break;
        case '/':
            if (_y == 0) _exitcode = -1;
            else
                _result = _x / _y;
            break;
        case '%':
            if (_y == 0) _exitcode = -2;
            else
                _result = _x % _y;
            break;
        default:
            break;
        }
        std::string result = std::to_string(_x) + _op +  std::to_string(_y) + "=" + std::to_string(_result) + "(exicode:" + std::to_string(_exitcode);
        std::cout << result << std::endl;
    }

private:
    int _x;//左操作数
    int _y;//右操作数
    char _op;//操作符
    int _result;//算数结果
    int _exitcode;//退出码
};

线程池内的线程在从任务队列拿出任务进行处理的过程,并不需要关心这些任务的类型和来源,只需要拿到任务后执行对应的Run方法即可。

主线程实现

主线程只需要不断向任务队列当中Push任务就行了,此后线程池当中的线程会从任务队列当中获取到这些任务并进行处理。

cpp 复制代码
#include "ThreadPoolv1.hpp"
#include "Task.hpp"
#include <memory>
#include <ctime>

using namespace std;

int main()
{
    std::unique_ptr<ThreadPool<Task>> tp(new ThreadPool<Task>());
    tp->Start();
    time(nullptr);
    const char* ops = "+-*/%";
    while(true)
    {
        int x, y;
        x = rand() % 50;
        y = rand() % 50;
        char op = ops[rand()%5];
        Task t(x, y, op);
        tp->PushTask(t);
        sleep(1);
    }
    return 0;
}

运行代码后会产生六个线程,其中一个是主线程,另外五个是线程池内处理任务的线程:

相关推荐
丁卯40441 分钟前
Go语言中使用viper绑定结构体和yaml文件信息时,标签的使用
服务器·后端·golang
chengooooooo42 分钟前
苍穹外卖day8 地址上传 用户下单 订单支付
java·服务器·数据库
李白同学2 小时前
【C语言】结构体内存对齐问题
c语言·开发语言
人间打气筒(Ada)2 小时前
MySQL主从架构
服务器·数据库·mysql
楼台的春风3 小时前
【MCU驱动开发概述】
c语言·驱动开发·单片机·嵌入式硬件·mcu·自动驾驶·嵌入式
落笔画忧愁e3 小时前
FastGPT快速将消息发送至飞书
服务器·数据库·飞书
小冷爱学习!3 小时前
华为动态路由-OSPF-完全末梢区域
服务器·网络·华为
技术小齐4 小时前
网络运维学习笔记 016网工初级(HCIA-Datacom与CCNA-EI)PPP点对点协议和PPPoE以太网上的点对点协议(此处只讲华为)
运维·网络·学习
ITPUB-微风4 小时前
Service Mesh在爱奇艺的落地实践:架构、运维与扩展
运维·架构·service_mesh
打不了嗝 ᥬ᭄4 小时前
Linux的权限
linux