Linux 线程池

目录

一、线程池的概念

二、线程池的优点

三、线程池的应用场景

四、线程池的实现

[1. 线程池的基本结构](#1. 线程池的基本结构)

[2. 代码实现](#2. 代码实现)

[3. 任务类的设计](#3. 任务类的设计)

[4. 主线程逻辑](#4. 主线程逻辑)

五、常见问题


一、线程池的概念

线程池是一种线程使用模式,它通过维护一组预先创建的线程来高效地处理任务。线程池的核心思想是避免频繁地创建和销毁线程,因为线程的创建和销毁会带来系统调度开销,并可能影响缓存局部性和整体性能。线程池中的线程处于等待状态,当有任务需要处理时,线程池会将任务分配给空闲的线程执行。


🍌上面这张图片展示了一个典型的线程池(ThreadPool)工作原理:

  1. 调用线程(调用线程-1、调用线程-2、...、调用线程-N)
  • 这些是客户端或应用程序中的线程,它们负责将任务提交到线程池中。

  • 每个调用线程将任务(例如,一个可执行的代码块或任务对象)提交到线程池的队列中。

  1. 线程池
  • 线程池是一个容器,用于管理一组预创建的线程(称为"处理线程"),这些线程可以重复使用来执行任务。

  • 线程池的主要目的是减少频繁创建和销毁线程的开销,提高系统性能。

  1. 队列
  • 队列是线程池中的一个核心组件,用于存储提交的任务。

  • 当调用线程提交任务时,任务会被放入队列中等待处理。

  • 队列通常是先进先出(FIFO)的,但具体实现可能根据线程池的配置有所不同。

  1. 处理线程(处理线程-1、处理线程-2、...、处理线程-M)
  • 这些是线程池中预先创建的线程,负责从队列中取出任务并执行。

  • 每个处理线程会不断检查队列中是否有任务,如果有,则取出任务并执行;如果没有,则进入等待状态。

  • 处理线程的数量(M)通常是固定的,由线程池的配置决定。

  1. 工作流程

  2. 任务提交:调用线程将任务提交到线程池的队列中。

  3. 任务存储:任务被存储在队列中,等待处理。

  4. 任务分配:处理线程从队列中取出任务并执行。

  5. 任务完成:处理线程完成任务后,继续从队列中获取新的任务。


二、线程池的优点

  1. 避免短任务的线程开销:对于短时间任务,线程池可以复用线程,避免频繁创建和销毁线程的代价。

  2. 充分利用系统资源:线程池可以根据系统资源(如处理器核心数、内存等)合理分配线程数量,避免资源浪费。

  3. 防止过度调度:线程池通过限制线程数量,避免因线程过多导致的调度开销。

三、线程池的应用场景

🌲线程池适用于以下场景:

  1. 大量短任务:例如Web服务器处理网页请求,任务数量巨大但单个任务处理时间短。

  2. 高性能要求:例如服务器需要快速响应客户端请求。

  3. 突发性大量请求:例如短时间内大量客户端请求,线程池可以避免因创建大量线程导致的系统崩溃。

四、线程池的实现

1. 线程池的基本结构

🍑线程池的核心组件包括:

  • 任务队列:存储待处理的任务。

  • 线程池:维护一组线程,负责从任务队列中获取任务并执行。

  • 互斥锁和条件变量:用于保护任务队列,确保线程安全。

2. 代码实现

cpp 复制代码
#pragma once

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

#define NUM 5  // 默认线程池中线程的数量

// 线程池类模板
template<class T>
class ThreadPool
{
private:
    // 判断任务队列是否为空
    bool IsEmpty()
    {
        return _task_queue.size() == 0;
    }

    // 锁定任务队列
    void LockQueue()
    {
        pthread_mutex_lock(&_mutex);
    }

    // 解锁任务队列
    void UnLockQueue()
    {
        pthread_mutex_unlock(&_mutex);
    }

    // 等待条件变量
    void Wait()
    {
        pthread_cond_wait(&_cond, &_mutex);
    }

    // 唤醒条件变量
    void WakeUp()
    {
        pthread_cond_signal(&_cond);
    }

public:
    // 构造函数
    ThreadPool(int num = NUM)
        : _thread_num(num)
    {
        // 初始化互斥锁和条件变量
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }

    // 析构函数
    ~ThreadPool()
    {
        // 销毁互斥锁和条件变量
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

    // 线程池中线程的执行例程(静态方法)
    static void* Routine(void* arg)
    {
        pthread_detach(pthread_self());  // 将线程设置为分离状态
        ThreadPool* self = (ThreadPool*)arg;  // 获取线程池对象

        // 线程不断从任务队列中获取任务并执行
        while (true)
        {
            self->LockQueue();  // 锁定任务队列
            while (self->IsEmpty())  // 如果任务队列为空,等待
            {
                self->Wait();
            }
            T task;
            self->Pop(task);  // 从任务队列中取出任务
            self->UnLockQueue();  // 解锁任务队列

            task.Run();  // 执行任务
        }
        return nullptr;
    }

    // 初始化线程池中的线程
    void ThreadPoolInit()
    {
        pthread_t tid;
        for (int i = 0; i < _thread_num; i++)
        {
            // 创建线程并传入线程池对象的this指针
            pthread_create(&tid, nullptr, Routine, this);
        }
    }

    // 向任务队列中添加任务
    void Push(const T& task)
    {
        LockQueue();  // 锁定任务队列
        _task_queue.push(task);  // 将任务加入队列
        UnLockQueue();  // 解锁任务队列
        WakeUp();  // 唤醒一个等待的线程
    }

    // 从任务队列中取出任务
    void Pop(T& task)
    {
        task = _task_queue.front();  // 获取队列头部任务
        _task_queue.pop();  // 移除队列头部任务
    }

private:
    std::queue<T> _task_queue;  // 任务队列
    int _thread_num;  // 线程池中线程的数量
    pthread_mutex_t _mutex;  // 互斥锁
    pthread_cond_t _cond;  // 条件变量
};

3. 任务类的设计

任务类需要包含一个Run方法,用于执行任务的逻辑:

cpp 复制代码
#pragma once

#include <iostream>

// 任务类
class Task
{
public:
    // 构造函数
    Task(int x = 0, int y = 0, char op = 0)
        : _x(x), _y(y), _op(op)
    {}

    // 析构函数
    ~Task()
    {}

    // 执行任务的方法
    void Run()
    {
        int result = 0;
        switch (_op)
        {
        case '+':
            result = _x + _y;
            break;
        case '-':
            result = _x - _y;
            break;
        case '*':
            result = _x * _y;
            break;
        case '/':
            if (_y == 0)
            {
                std::cerr << "Error: division by zero!" << std::endl;
                return;
            }
            else
            {
                result = _x / _y;
            }
            break;
        case '%':
            if (_y == 0)
            {
                std::cerr << "Error: modulo by zero!" << std::endl;
                return;
            }
            else
            {
                result = _x % _y;
            }
            break;
        default:
            std::cerr << "Error: invalid operation!" << std::endl;
            return;
        }
        std::cout << "Thread [" << pthread_self() << "]: " << _x << " " << _op << " " << _y << " = " << result << std::endl;
    }

private:
    int _x;  // 操作数1
    int _y;  // 操作数2
    char _op;  // 操作符
};

4. 主线程逻辑

主线程负责向任务队列中添加任务,线程池中的线程会自动处理这些任务:

cpp 复制代码
#include "Task.hpp"
#include "ThreadPool.hpp"

int main()
{
    srand((unsigned int)time(nullptr));  // 初始化随机数种子

    // 创建线程池并初始化
    ThreadPool<Task>* tp = new ThreadPool<Task>;
    tp->ThreadPoolInit();

    const char* op = "+-*/%";  // 操作符列表

    // 不断向任务队列中添加任务
    while (true)
    {
        sleep(1);  // 每秒添加一个任务
        int x = rand() % 100;  // 随机生成操作数1
        int y = rand() % 100;  // 随机生成操作数2
        int index = rand() % 5;  // 随机选择操作符
        Task task(x, y, op[index]);  // 创建任务
        tp->Push(task);  // 将任务加入线程池
    }

    return 0;
}

五、常见问题

  1. 为什么在线程池的Routine函数中使用while循环检查任务队列是否为空?
  • 原因 :条件变量可能存在伪唤醒 (即线程被唤醒不是因为条件满足,而是由于系统信号或其他原因)。使用while循环可以确保在被唤醒后再次检查条件,避免任务队列实际为空时错误地执行任务。

  • 代码示例

    cpp 复制代码
    while (self->IsEmpty()) 
    {
        self->Wait();
    }
  • 对比if :如果使用if,伪唤醒可能导致线程试图从空队列中取任务,引发未定义行为(如崩溃)。

  1. 为什么Routine函数需要是静态方法?如何访问类的成员?
  • C++限制pthread_create要求线程函数是static,因为它没有隐式的this指针。

  • 访问成员 :通过将线程池对象的指针(this)作为参数传递给Routine,可以在静态方法中访问非静态成员:

    cpp 复制代码
    static void* Routine(void* arg) 
    {
        ThreadPool* self = (ThreadPool*)arg;
        self->LockQueue(); // 访问成员函数
    }
  1. 互斥锁和条件变量的作用是什么?
  • 互斥锁(pthread_mutex_t :保护任务队列(_task_queue),确保同一时间只有一个线程访问队列,防止数据竞争。

  • 条件变量(pthread_cond_t :协调线程的等待与唤醒。当队列为空时,线程通过pthread_cond_wait挂起;当新任务加入时,通过pthread_cond_signal唤醒一个线程。

  1. 如何扩展线程池以处理不同类型的任务?
  • 模板设计 :当前线程池使用模板类ThreadPool<T>,只需为不同任务类型实现对应的Run方法即可。

  • 示例:若需处理网络请求,可以定义新的任务类:

    cpp 复制代码
    class NetworkTask 
    {
    public:
        void Run() 
        {
            // 处理网络请求的逻辑
        }
    };
    ThreadPool<NetworkTask> network_pool;
  1. 条件变量为何使用signal而非broadcast
  • 避免惊群效应pthread_cond_signal唤醒一个线程,而pthread_cond_broadcast唤醒所有等待线程。使用signal减少不必要的竞争,尤其在任务队列中每次只添加一个任务时更高效。
  1. 主线程中的sleep(1)是否合理?
  • 模拟场景 :示例中sleep(1)用于降低任务生成速度,便于观察输出。实际应用中,应根据需求调整任务生产速率(如事件驱动或实时接收请求)。
相关推荐
CodeWithMe36 分钟前
【Linux C】简单bash设计
linux·c语言·bash
秃头的赌徒38 分钟前
Docker 前瞻
linux·运维·服务器
normaling39 分钟前
十一,Shell
linux
家庭云计算专家1 小时前
IPV6应用最后的钥匙:DDNS-GO 动态域名解析工具上手指南--家庭云计算专家
linux·服务器·云计算·编辑器
青山瀚海1 小时前
windows中搭建Ubuntu子系统
linux·windows·ubuntu·docker
xxxx1234452 小时前
Linux驱动开发-网络设备驱动
linux·运维·驱动开发
2401_861615282 小时前
debian转移根目录
linux·debian·电脑
刘若水3 小时前
Linux: 线程控制
linux·运维·服务器
筱戥芊茹3 小时前
RK3588上Linux系统编译C/C++ Demo时出现BUG:The C/CXX compiler identification is unknown
linux·c语言·c++·嵌入式硬件·bug
__基本操作__3 小时前
linux以C方式和内核交互监听键盘[香橙派搞机日记]
linux·c语言·输入子系统