Linux应用编程基础08-线程池

49 阅读8分钟

线程池是一种管理线程的机制,它可以在需要时自动创建和销毁线程,以及分配和回收线程资源。线程池的主要优点是减少了频繁创建和销毁线程所带来的开销,提高了系统的稳定性和可扩展性。此外,线程池还可以有效地控制线程的数量,避免过多线程导致的资源竞争和系统过载

1、线程池介绍

1.1 池化技术

线程池就是提前创建一批线程,当任务来临时,线程直接从任务队列中获取任务执行,可以提高整体效率;同时一批线程会被合理维护,避免调度时造成额外开销

像这种把未来会高频使用到,并且创建较为麻烦的资源提前申请好的技术称为池化技术,池化技术 可以极大地提高性能,最典型的就是线程池,常用于各种涉及网络连接相关的服务中,比如 MySQL 连接池、HTTP 连接池、Redis 连接池等

池化技术的本质:空间换时间

1.2 线程池的优点

  1. 线程在使用前就已经创建好了,使用时直接将任务交给线程完成
  2. 线程会被合理调度,确保 任务与线程 间能做到负载均衡

线程池中的线程数量不是越多越好,因为线程增多会导致调度变复杂,具体创建多少线程取决于具体业务场景,比如 处理器内核、剩余内存、网络中的 socket 数量等

线程池 还可以配合生产者消费者模型一起使用,做到解耦与提高效率

image.png

1.3 应用场景

线程池 有以下几种应用场景:

  1. 存在大量且短小的任务请求,比如 Web 服务器中的网页请求,使用线程池就非常合适,因为网页点击量众多,并且大多都没有长时间连接访问
  2. 对性能要求苛刻,力求快速响应需求,比如游戏服务器,要求对玩家的操作做出快速响应
  3. 突发大量请求,但不至于使服务器产生过多的线程,短时间内,在服务器创建大量线程会使得内存达到极限,造成出错,可以使用线程池规避问题

2、线程池实现

2.1 简单实现

把线程池封装成一个对象:

// 线程池的两大核心:一批线程 与 任务队列
// 客户端发出请求,新增任务,线程获取任务,执行任务

#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <memory>
#include <unistd.h>
#include <pthread.h>
#include <functional>

#define THREAD_NUM 10

template <class T>
class MyThreadPool
{
    using func_t = std::function<void(T &)>; // 包装器,把函数封装成一个类成员
private:
    std::vector<pthread_t> _threads; // num个线程
    int _num;                        // 线程数量
    std::queue<T> _tasks;            // 任务队列,利用 STL 自动扩容的特性,无需担心容量
    pthread_mutex_t _mtx;            // 互斥锁:保证多个线程并访问任务队列时的线程安全
    pthread_cond_t _cond;            // 条件变量:可以在任务队列为空时,让一批线程进入等待状态,也就是线程同步
    func_t _func;

public:
    MyThreadPool(func_t func, int num = THREAD_NUM)
        : _threads(num), _num(num), _func(func)
    {
        // 初始化互斥锁和条件变量
        pthread_mutex_init(&_mtx, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }

    ~MyThreadPool()
    {
        // 互斥锁、条件变量
        pthread_mutex_destroy(&_mtx);
        pthread_cond_destroy(&_cond);
    }

    void init()
    {
        // 其他信息初始化(当前不需要)
    }

    void start()
    {
        // 启动线程池
        // 创建一批线程并启动
        for (int i = 0; i < _num; i++)
            // threadRoutine是一个静态函数,并没有this指针以访问类内成员,可以采取传递 this 指针的方式解决问题
            pthread_create(&_threads[i], nullptr, threadRoutine, this);
    }

    // 将用户需要执行的业务装载至 任务队列 中,等待线程执行
    // 装载任务
    void pushTask(const T &task)
    {
        // 本质上就是在生产商品,需要加锁保护
        pthread_mutex_lock(&_mtx);
        _tasks.push(task);

        // 唤醒消费者进行消费
        pthread_cond_signal(&_cond);
        pthread_mutex_unlock(&_mtx);
    }

    // 提供给线程的回调函数(设置为静态,否则线程调不动——参数不匹配)
    static void *threadRoutine(void *args)
    {
        // 避免等待线程,直接剥离(主线程无需等待次线程运行结束)
        pthread_detach(pthread_self());

        auto ptr = static_cast<MyThreadPool<T> *>(args); // 为了在static里访问类的成员变量

        while (true)
        {
            // 任务队列是临界资源,需要保护
            pthread_mutex_lock(&ptr->_mtx);

            // 等待条件满足
            while (ptr->_tasks.empty())
                pthread_cond_wait(&ptr->_cond, &ptr->_mtx);

            T task = ptr->_tasks.front();
            ptr->_tasks.pop();

            pthread_mutex_unlock(&ptr->_mtx);

            task();              // 这里是对()的一个运算符重载,执行任务,进行消费(不需要加锁,主动让出锁资源以提高整体效率)
            ptr->callBack(task); // 回调函数,打印结果
        }
    }

    func_t callBack(T &task)
    {
        _func(task);
    }
};

封装一个执行任务的Task.hpp

#pragma once

#include <string>

template <class T>
class Task
{
private:
    T _x;
    T _y;
    char _op; // 运算符
    T _res;   // 结果
    int _err; // 错误标识

public:
    Task(T x = 0, T y = 0, char op = '+')
        : _x(x), _y(y), _op(op), _res(0), _err(0){
    }

    // 重载运算操作
    void operator()(){
        // 简单计算
        switch (_op)
        {
        case '+':
            _res = _x + _y;
            break;
        case '-':
            _res = _x - _y;
            break;
        case '*':
            _res = _x * _y;
            break;
        case '/':
            if (_y == 0)
                _err = -1;
            else
                _res = _x / _y;
            break;
        case '%':
            if (_y == 0)
                _err = -2;
            else
                _res = _x % _y;
            break;
        default:
            _err = -3;
            break;
        }
    }

    // 获取计算结果
    std::string getResult()
    {
        // 根据错误标识,返回计算结果
        std::string ret = std::to_string(_x) + " " + _op + " " + std::to_string(_y);

        if (_err){
            ret += " error";

            // 判读是 / 错误还是 % 错误
            if (_err == -1)
                ret += " [-1] \t / 0 引发了错误";
            else if (_err == -2)
                ret += " [-2] \t % 0 引发了错误";
            else
                ret += " [-3] \t 不合法的操作符,只能为 [+-*/%]";
        }
        else{
            ret += " = " + std::to_string(_res);
        }

        return ret;
    }
};

使用线程池:

#include "ThreadPool.hpp"
#include "Task.hpp" // 封装一个执行任务的类
#include <memory>
// 回调函数(打印计算结果)
void callBack(Task<int> &task)
{
    // 获取计算结果后打印
    std::cout << "计算结果为: " << task.getResult() << std::endl;
}

int main()
{
    std::unique_ptr<MyThreadPool<Task<int>>> ptr(new MyThreadPool<Task<int>>(callBack));

    ptr->init();
    ptr->start();

    while (true)
    {
        // 输入 操作数 操作数 操作符
        int x = 0, y = 0;
        char op = '+';
        std::cout << "输入 x: ";
        std::cin >> x;
        std::cout << "输入 y: ";
        std::cin >> y;
        std::cout << "输入 op: ";
        std::cin >> op;

        // 构建任务对象(Task 任务类,实现基本的两数运算)
        Task<int> task(x, y, op);

        // 装载任务
        ptr->pushTask(task);
    }

    return 0;
}

2.2 结合生产者消费者模型

让线程池专注于任务处理,至于如何确保任务装载及获取时的线程安全问题,交给生产者消费者模型(基于阻塞队列)

BlockQueue.hpp

#pragma once

#include <queue>
#include <mutex>
#include <pthread.h>
#include "LockGuard.hpp"

#define DEF_SIZE 10

template <class T>
class BlockQueue
{
private:
    std::queue<T> _queue;
    size_t _cap;              // 阻塞队列的容量
    pthread_mutex_t _mtx;     // 互斥锁
    pthread_cond_t _pro_cond; // 生产者条件变量
    pthread_cond_t _con_cond; // 消费者条件变量
public:
    BlockQueue(size_t cap = DEF_SIZE)
        : _cap(cap)
    {
        // 初始化锁与条件变量
        pthread_mutex_init(&_mtx, nullptr);
        pthread_cond_init(&_pro_cond, nullptr);
        pthread_cond_init(&_con_cond, nullptr);
    }

    ~BlockQueue()
    {
        // 销毁锁与条件变量
        pthread_mutex_destroy(&_mtx);
        pthread_cond_destroy(&_pro_cond);
        pthread_cond_destroy(&_con_cond);
    }

    // 生产数据(入队)
    void Push(const T &inData)
    {
        // 加锁(RAII风格,利用类的析构函数来实现自动解锁)
        LockGuard lock(&_mtx);

        // 循环判断条件是否满足
        while (IsFull())
        {
            pthread_cond_wait(&_pro_cond, &_mtx);
        }

        _queue.push(inData);

        // 唤醒
        pthread_cond_signal(&_con_cond);

        // 自动解锁
    }

    // 消费数据(出队)
    void Pop(T *outData)
    {
        // 加锁
        LockGuard lock(&_mtx);

        // 循环判读条件是否满足
        while (IsEmpty())
        {
            pthread_cond_wait(&_con_cond, &_mtx);
        }

        *outData = _queue.front();
        _queue.pop();

        // 唤醒
        pthread_cond_signal(&_pro_cond);

        // 自动解锁
    }

private:
    // 判断是否为满
    bool IsFull()
    {
        return _queue.size() == _cap;
    }

    // 判断是否为空
    bool IsEmpty()
    {
        return _queue.empty();
    }
};

LockGuard.hpp

#pragma once

#include <pthread.h>

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *pmtx)
        : _pmtx(pmtx)
    {
        // 加锁
        pthread_mutex_lock(_pmtx);
    }

    ~LockGuard()
    {
        // 解锁
        pthread_mutex_unlock(_pmtx);
    }

private:
    pthread_mutex_t *_pmtx;
};

引入自己封装实现的线程库 Thread.hpp,支持对线程做出更多操作

Thread.hpp

#pragma once

#include <iostream>
#include <string>
#include <pthread.h>

enum class Status
{
    NEW = 0, // 新建
    RUNNING, // 运行中
    EXIT     // 已退出
};

// 参数、返回值为 void 的函数类型
typedef void (*func_t)(void *);

class Thread
{
private:
    pthread_t _tid;    // 线程 ID
    std::string _name; // 线程名
    Status _status;    // 线程状态
    func_t _func;      // 线程回调函数
    void *_args;       // 传递给回调函数的参数
    
public:
    Thread(int num = 0, func_t func = nullptr, void *args = nullptr)
        : _tid(0), _status(Status::NEW), _func(func), _args(args){
        // 根据编号写入名字
        char name[128];
        snprintf(name, sizeof name, "thread-%d", num);
        _name = name;
    }

    ~Thread(){
    }

    // 获取 ID
    pthread_t getTID() const{
        return _tid;
    }

    // 获取线程名
    std::string getName() const{
        return _name;
    }

    // 获取状态
    Status getStatus() const{
        return _status;
    }

    // 回调方法
    static void *runHelper(void *args){
        Thread *myThis = static_cast<Thread *>(args);

        // 很简单,回调用户传进来的 func 函数即可
        myThis->_func(myThis->_args);
    }

    // 启动线程
    void run(){
        int ret = pthread_create(&_tid, nullptr, runHelper, this);
        if (ret != 0)
        {
            std::cerr << "create thread fail!" << std::endl;
            exit(1); // 创建线程失败,直接退出
        }
        _status = Status::RUNNING; // 更改状态为 运行中
    }

    // 线程等待
    void join(){
        int ret = pthread_join(_tid, nullptr);
        if (ret != 0)
        {
            std::cerr << "thread join fail!" << std::endl;
            exit(1); // 等待失败,直接退出
        }
        _status = Status::EXIT; // 更改状态为 退出
    }
};

改进后的线程池

ThreadPool.hpp

#pragma once

#include <vector>
#include <string>
#include <memory>
#include <functional>
#include <unistd.h>
#include <pthread.h>
#include "Task.hpp"
#include "Thread.hpp"
#include "BlockingQueue.hpp" // CP模型

#define THREAD_NUM 10

template <class T>
class ThreadPool
{
    using func_t = std::function<void(T &)>; // 包装器

private:
    std::vector<Thread> _threads;
    int _num;                  // 线程数量
    BlockQueue<T> _blockqueue; // 阻塞队列
    func_t _func;

public:
    ThreadPool(func_t func, int num = THREAD_NUM)
        : _num(num), _func(func){
    }

    ~ThreadPool(){
        // 等待线程退出
        for (auto &t : _threads)
            t.join();
    }

    void init(){
        // 创建一批线程
        for (int i = 0; i < _num; i++)
            _threads.push_back(Thread(i, threadRoutine, this));
    }

    void start(){
        // 启动线程
        for (auto &t : _threads)
            t.run();
    }

    // 提供给线程的回调函数(已修改返回类型为 void)
    static void threadRoutine(void *args){
        // 避免等待线程,直接剥离
        pthread_detach(pthread_self());

        auto ptr = static_cast<ThreadPool<T> *>(args);

        while (true){
            // 从CP模型中获取任务
            T task = ptr->popTask();

            task();
            ptr->callBack(task); // 回调函数
        }
    }

    // 装载任务
    void pushTask(const T &task){
        _blockqueue.Push(task);
    }

protected:
    func_t callBack(T &task){
        _func(task);
    }

    T popTask(){
        T task;
        _blockqueue.Pop(&task);

        return task;
    }
};

使用线程池

main.cpp

#include "ThreadPool.hpp"
#include <memory>
#include <iostream>
typedef Task<int> type;

// 回调函数
void callBack(type &task){
    // 获取计算结果后打印
    std::cout << "计算结果为: " << task.getResult() << std::endl;
}

int main(){
    std::unique_ptr<ThreadPool<type>> ptr(new ThreadPool<type>(callBack));

    ptr->init();
    ptr->start();

    while (true){
        // 输入 操作数 操作数 操作符
        int x = 0, y = 0;
        char op = '+';
        std::cout << "输入 x: ";
        std::cin >> x;
        std::cout << "输入 y: ";
        std::cin >> y;
        std::cout << "输入 op: ";
        std::cin >> op;

        // 构建任务对象
        type task(x, y, op);

        // 装载任务
        ptr->pushTask(task);
    }

    return 0;
}