线程池的实现与解析:基于C++和pthread的线程池设计

目录

一、概述

二、线程池的基本原理

三、线程池工作情景

四、线程池的实现细节

(一)数据结构设计

(二)单例模式的实现

(三)线程池的初始化

(四)任务的提交与执行

(五)线程同步机制

五、线程池的工作流程

六、完整代码实现

七、注意事项

八、总结


一、概述

线程池是一种非常重要的并发编程技术,它通过预先创建一组线程来处理任务,从而避免了频繁创建和销毁线程的开销,提高了程序的性能和资源利用率。本文将详细介绍一个基于C++和pthread库实现的线程池,包括其原理、实现细节、工作流程以及注意事项。

二、线程池的基本原理

线程池的核心思想是将线程的创建和管理与任务的执行分离。线程池预先创建一定数量的工作线程,这些线程在没有任务时处于空闲状态,等待任务的到来。当有任务提交到线程池时,线程池会从任务队列中取出一个任务并分配给一个空闲线程执行。任务执行完成后,线程会返回线程池,等待下一个任务。

线程池的主要优点包括:

  1. 减少线程创建和销毁的开销:线程的创建和销毁是相对耗时的操作,线程池通过复用线程,避免了频繁的线程创建和销毁

  2. 提高程序性能:线程池中的线程可以快速响应任务,减少了任务的等待时间

  3. 限制并发数量:线程池可以限制同时运行的线程数量,避免系统资源耗尽

  4. 提高资源利用率:通过合理管理线程,线程池可以更好地利用系统资源

线程池解决以下问题:

  1. 解决任务处理

  2. 阻塞IO

  3. 解决线程创建于销毁的成本问题

  4. 管理线程

三、线程池工作情景

假设我们的线程池有 3个线程,任务队列目前 不限制大小。

情况一:风平浪静,大家都在摸鱼

线程池已经启动,三个线程也到位了,但主线程什么任务也没push进来。任务队列是空的,三个线程蹲在那儿傻等,啥也不干。

情况二:任务来了,正好够分

主线程突然来了三个任务,向队列里push任务。三个线程立马被唤醒,一个一个冲出来取任务。任务队列瞬间被清空,三个线程各自扛着任务去干活了。主线程也轻松了,因为它没多扔任务,线程池刚好吃得下。

情况三:任务太多,先排队等

三个线程全在干活,结果主线程又扔进来一个任务。这时候线程池已经没空闲线程了,新任务只能乖乖进队列排队。等三个线程有人干完活了,就会主动从队列里再取一个任务继续干。这就是线程池的“先上岗,后排队”策略,妥妥的“活人等任务,任务等活人”。

情况四:任务爆仓,主线程被迫停摆

情况比较极端,先假设任务队列被我们设置了最大容量限制。这时候线程池的三个线程全在忙,队列也满。主线程又想扔任务进来,发现没地方放,也没人接,只能原地等待,等队列腾出位置。

四、线程池的实现细节

(一)数据结构设计

线程池的主要数据结构包括:

  • 线程列表:存储线程池中所有线程的信息。

  • 任务队列:存储待执行的任务。

  • 互斥锁:用于保护任务队列的线程安全。

  • 条件变量:用于实现线程的等待和唤醒机制。

以下是线程池的基本数据结构定义:

template <class T>
class ThreadPool
{
private:
    std::vector<ThreadInfo> _threads; // 线程列表
    std::queue<T> _tasks; // 任务队列
    pthread_mutex_t _mutex; // 互斥锁
    pthread_cond_t _cond; // 条件变量
    static ThreadPool<T> *_tp; // 线程池实例指针
    static pthread_mutex_t Instance_mutex; // 用于保护单例实例的互斥锁
};

// 初始化线程池实例指针为nullptr
template <class T>
ThreadPool<T> *ThreadPool<T>::_tp = nullptr;

// 初始化单例保护互斥锁
template <class T>
pthread_mutex_t ThreadPool<T>::Instance_mutex = PTHREAD_MUTEX_INITIALIZER;

在本文的实现中,任务队列使用了std::queue,线程列表使用了std::vector,互斥锁和条件变量使用了pthread_mutex_tpthread_cond_t

(二)单例模式的实现

单例模式是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点。单例模式有两种常见的实现方式:饿汉模式和懒汉模式。

饿汉模式

工作原理:饿汉模式在程序启动时就创建单例对象,确保对象在程序运行期间始终存在。 解决的问题:避免了懒汉模式中可能出现的线程安全问题,因为对象在程序启动时就已经创建,不会出现多线程同时创建对象的情况。 缺点:即使程序中从未使用过单例对象,也会占用内存。

懒汉模式

工作原理:懒汉模式在第一次访问单例对象时才创建对象,延迟了对象的创建时间。 解决的问题:节省了内存资源,只有在需要时才会创建对象。 缺点:需要额外的线程同步机制来确保多线程环境下的线程安全。

在本文的线程池实现中,我们使用了懒汉模式。以下是单例模式的实现代码:

template <class T>
class ThreadPool
{
public:
    // 获取单例实例
    static ThreadPool<T> *GetInstance()
    {
        // 双重检查锁定模式,确保线程安全
        if (_tp == nullptr)
        {
            pthread_mutex_lock(&Instance_mutex);
            if (_tp == nullptr)
            {
                _tp = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&Instance_mutex);
        }
        return _tp;
    }

private:
    // 私有构造函数,防止外部直接构造
    ThreadPool(int num = defaultnum)
        : _threads(num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }

    // 私有析构函数,防止外部直接析构
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

    // 禁止拷贝构造和赋值操作
    ThreadPool(const ThreadPool<T> &) = delete;
    const ThreadPool<T> &operator=(ThreadPool<T> &) = delete;

    // 静态成员变量
    static ThreadPool<T> *_tp; // 线程池实例指针
    static pthread_mutex_t Instance_mutex; // 用于保护单例实例的互斥锁
};

代码注释

  • GetInstance方法

    • 首先检查单例对象是否已经创建,如果未创建,则加锁。

    • 在加锁后再次检查,确保只有一个线程能够创建对象。

    • 创建对象后解锁,返回单例对象。

  • _tp:静态指针,存储单例对象。

  • Instance_mutex:静态互斥锁,用于保护单例对象的创建过程。

(三)线程池的初始化

线程池的初始化包括以下步骤:

  1. 初始化互斥锁和条件变量。

  2. 创建线程列表。

  3. 启动线程池中的每个线程。

在代码中,线程池的初始化通过构造函数和Start方法完成。构造函数负责初始化互斥锁和条件变量,并创建线程列表。Start方法负责启动线程池中的每个线程。

template <class T>
class ThreadPool
{
    // 启动线程池中的所有线程
    void Start()
    {
        int num = _threads.size(); // 获取线程池中线程的数量
        for (int i = 0; i < num; i++) // 遍历线程列表
        {
            _threads[i].name = "thread-" + std::to_string(i + 1); // 设置线程名称
            pthread_create(&(_threads[i].tid), nullptr, HandleTask, this); // 创建线程
        }
    }
};

线程创建函数 pthread_create 要求传一个 void* (*)(void*) 类型的函数,但 C++ 类的成员函数默认带了个 this 指针,因此我们要使用static 函数。static 函数没有 this 指针,但我们可以手动传个 this 进去,然后调用真正的成员函数,就像“代理登录”。

(四)任务的提交与执行

任务的提交和执行是线程池的核心功能。当有任务提交到线程池时,线程池会将任务加入任务队列,并唤醒一个等待中的线程来执行任务。任务执行完成后,线程会返回线程池,等待下一个任务。

在代码中,任务的提交通过Push方法完成,该方法将任务加入任务队列,并调用Wakeup方法唤醒一个等待中的线程。任务的执行通过HandleTask函数完成,该函数从任务队列中取出任务并执行。

template <class T>
class ThreadPool
{
    // 线程池中线程的处理任务函数
    static void *HandleTask(void *args)
    {
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args); // 将参数转换为线程池指针
        std::string name = tp->GetThreadName(pthread_self());   // 获取当前线程的名称

        // 线程主循环,不断从任务队列中获取任务并执行
        while (true)
        {
            tp->Lock(); // 加锁,保护任务队列
            while (tp->IsQueueEmpty()) // 如果任务队列为空
            {
                tp->Sleep(); // 线程进入等待状态
            }
            T t = tp->Pop(); // 从任务队列中取出一个任务
            tp->Unlock(); // 解锁,释放任务队列
            t(); // 执行任务
            std::cout << name << " run, " << "result: " << t.GetResult() << std::endl; // 输出任务执行结果
        }
        return nullptr; // 线程函数返回值
    }
};

(五)线程同步机制

线程池中使用了互斥锁和条件变量来实现线程同步。互斥锁用于保护任务队列的线程安全,条件变量用于实现线程的等待和唤醒机制。

在代码中,LockUnlock方法用于加锁和解锁,Sleep方法用于线程等待,Wakeup方法用于线程唤醒。

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 Sleep()
    {
        pthread_cond_wait(&_cond, &_mutex);
    }
    // 检查任务队列是否为空
    bool IsQueueEmpty()
    {
        return _tasks.empty();
    }
};

细节补充:

  • 条件变量的使用pthread_cond_wait需要与互斥锁配合使用,确保线程在等待时不会出现竞态条件。

  • 唤醒机制pthread_cond_signal只会唤醒一个等待中的线程,如果需要唤醒所有等待中的线程,可以使用pthread_cond_broadcast

五、线程池的工作流程

线程池的工作流程如下:

  1. 线程池初始化:创建线程池实例,初始化互斥锁和条件变量,创建线程列表。

  2. 任务提交:调用Push方法将任务加入任务队列,并唤醒一个等待中的线程。

  3. 任务执行:线程从任务队列中取出任务并执行,执行完成后返回线程池等待下一个任务。

  4. 线程等待:当任务队列为空时,线程进入等待状态,直到有新任务加入。

  5. 线程唤醒:当有新任务加入任务队列时,调用Wakeup方法唤醒一个等待中的线程。

六、完整代码实现

以下是线程池的完整代码实现:

#pragma once
#include <iostream>
#include <pthread.h>
#include <vector>
#include <string>
#include <queue>
#include <unistd.h>

struct ThreadInfo
{
    pthread_t tid;
    std::string name;
};
static const int defaultnum = 5;

template <class T>
class ThreadPool
{
    // 线程池中线程的处理任务函数
    static void *HandleTask(void *args)
    {
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args); // 将参数转换为线程池指针
        std::string name = tp->GetThreadName(pthread_self());   // 获取当前线程的名称

        // 线程主循环,不断从任务队列中获取任务并执行
        while (true)
        {
            tp->Lock(); // 加锁,保护任务队列
            while (tp->IsQueueEmpty()) // 如果任务队列为空
            {
                tp->Sleep(); // 线程进入等待状态
            }
            T t = tp->Pop(); // 从任务队列中取出一个任务
            tp->Unlock(); // 解锁,释放任务队列
            t(); // 执行任务
            std::cout << name << " run, " << "result: " << t.GetResult() << std::endl; // 输出任务执行结果
        }
        return nullptr; // 线程函数返回值
    }

public:
    // 获取单例实例
    static ThreadPool<T> *GetInstance()
    {
        // 双重检查锁定模式,确保线程安全
        if (_tp == nullptr)
        {
            pthread_mutex_lock(&Instance_mutex);
            if (_tp == nullptr)
            {
                _tp = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&Instance_mutex);
        }
        return _tp;
    }

    // 启动线程池中的所有线程
    void Start()
    {
        int num = _threads.size(); // 获取线程池中线程的数量
        for (int i = 0; i < num; i++) // 遍历线程列表
        {
            _threads[i].name = "thread-" + std::to_string(i + 1); // 设置线程名称
            pthread_create(&(_threads[i].tid), nullptr, HandleTask, this); // 创建线程
        }
    }

    // 向任务队列中添加一个任务
    void Push(const T &t)
    {
        Lock(); // 加锁,保护任务队列
        _tasks.push(t); // 将任务加入队列
        Wakeup(); // 唤醒一个等待中的线程
        Unlock(); // 解锁,释放任务队列
    }

    // 从任务队列中取出一个任务
    T Pop()
    {
        T t = _tasks.front();
        _tasks.pop();
        return t;
    }

    // 加锁操作,保护任务队列
    void Lock()
    {
        pthread_mutex_lock(&_mutex);
    }

    // 解锁操作,释放任务队列
    void Unlock()
    {
        pthread_mutex_unlock(&_mutex);
    }

    // 唤醒一个等待中的线程
    void Wakeup()
    {
        pthread_cond_signal(&_cond);
    }

    // 线程进入等待状态
    void Sleep()
    {
        pthread_cond_wait(&_cond, &_mutex);
    }

    // 检查任务队列是否为空
    bool IsQueueEmpty()
    {
        return _tasks.empty();
    }

    // 获取线程名称
    std::string GetThreadName(pthread_t tid)
    {
        std::string res = "\0";
        for (const auto &t : _threads)
        {
            if (t.tid == tid)
                res = t.name;
        }
        return res;
    }

private:
    // 私有构造函数,防止外部直接构造
    ThreadPool(int num = defaultnum)
        : _threads(num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }

    // 私有析构函数,防止外部直接析构
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

    // 禁止拷贝构造和赋值操作
    ThreadPool(const ThreadPool<T> &) = delete;
    const ThreadPool<T> &operator=(ThreadPool<T> &) = delete;

    // 静态成员变量
    static ThreadPool<T> *_tp; // 线程池实例指针
    static pthread_mutex_t Instance_mutex; // 用于保护单例实例的互斥锁

    // 线程池的成员变量
    std::vector<ThreadInfo> _threads; // 线程列表
    std::queue<T> _tasks; // 任务队列
    pthread_mutex_t _mutex; // 互斥锁
    pthread_cond_t _cond; // 条件变量
};

// 初始化静态成员变量
template <class T>
ThreadPool<T> *ThreadPool<T>::_tp = nullptr;

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

代码注释

  • GetInstance方法

    • 使用双重检查锁定模式(Double-Checked Locking)确保线程安全。

    • 首先检查单例对象是否已经创建,如果未创建,则加锁。

    • 在加锁后再次检查,确保只有一个线程能够创建对象。

    • 创建对象后解锁,返回单例对象。

  • Start方法

    • 遍历线程列表,为每个线程设置名称并启动线程。

  • Push方法

    • 加锁保护任务队列,将任务加入队列,唤醒一个等待中的线程,解锁释放任务队列。

  • Pop方法

    • 从任务队列中取出一个任务。

  • LockUnlockWakeupSleep方法

    • 提供线程同步机制,确保任务队列的线程安全。

  • GetThreadName方法

    • 通过线程ID获取线程名称。

七、注意事项

  1. 线程安全:任务队列的访问必须加锁,确保线程安全。

  2. 资源管理:线程池的生命周期管理需要谨慎,避免资源泄漏。

  3. 任务类型:任务类型T必须是可调用对象,并且需要提供GetResult方法。

  4. 线程数量:线程池的大小需要根据系统资源和任务类型合理配置。

  5. 异常处理:任务执行过程中可能抛出异常,需要在HandleTask中处理异常,避免线程崩溃。

八、总结

线程池是一种非常重要的并发编程技术,它通过预先创建一组线程来处理任务,从而避免了频繁创建和销毁线程的开销,提高了程序的性能和资源利用率。本文详细介绍了线程池的基本原理、实现细节、工作流程以及注意事项,并给出了一个基于C++和pthread库的线程池实现。通过合理使用线程池,可以显著提升并发程序的性能和稳定性。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值