Linux之线程池-CSDN博客

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

线程池

线程池概念

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

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

这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

线程池的应用场景

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

线程池实现原理

线程池通过一个线程安全的阻塞任务队列加上一个或一个以上的线程实现线程池中的线程可以从阻塞队列中获取任务进行任务处理当线程都处于繁忙状态时可以将任务加入阻塞队列中等到其它的线程空闲后进行处理。
在这里插入图片描述

testMain.cc

主线程任务逻辑启动线程不断向任务队列中push任务就可以了此时线程接收到任务就会进行处理

#include <iostream>
#include <ctime>
#include <unistd.h>
#include "threadPool.hpp"
#include "Task.hpp"
#include "log.hpp"

int main()
{
    srand((unsigned int)time(nullptr) ^ getpid());
    ThreadPool<Task>* tp = new ThreadPool<Task>();

    //启动线程
    tp->run();

    //主线程执行任务
    while(true)
    {
        int x = rand() % 100 + 1;
        usleep(1000);
        int y = rand() % 50 + 1;
        Task t(x, y, [](int x, int y)->int{
            return x + y;
        });

        logMessage(DEBUG, "制作任务完成%d+%d=?", x, y);
        // std::cout << "制作任务完成: " << x << "+" << y << "=?" << std::endl;

        //将任务推送到线程池中
        tp->pushTask(t);

        sleep(1);
    }
    return 0;
}

thread.hpp

我们对创建线程进行封装包含线程名线程个数回调函数线程ID等

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <cstdio>

typedef void *(*func_t)(void *);

class ThreadData
{
public:
    std::string name_;
    void *args_;
};

class Thread
{
public:
    Thread(int num, func_t callback, void *args) : func_(callback)
    {
        char nameBuffer[64];
        snprintf(nameBuffer, sizeof nameBuffer, "Thread-%d", num);
        name_ = nameBuffer;

        tdata_.args_ = args;
        tdata_.name_ = name_;
    }

    void start()
    {
        pthread_create(&tid_, nullptr, func_, (void *)&tdata_);
    }

    void join()
    {
        pthread_join(tid_, nullptr);
    }

    std::string name()
    {
        return name_;
    }

    ~Thread()
    {
    }

private:
    std::string name_; // 线程名
    int num_;          // 线程个数
    func_t func_;      // 回调函数
    pthread_t tid_;    // 线程ID
    ThreadData tdata_;
};

threadPool.hpp

线程池中我们需要用注意的是

  1. 需要用到条件变量与互斥锁因为线程池中的任务队列会被多个执行流访问所以我们必须引入互斥锁
  2. 当线程池中任务队列为满时我们此时push任务就无法push进去此时就需要挂起等待直到线程将某一任务执行完毕唤醒等待队列才可以继续进行push我们执行任务也是一样只有当任务队列中有任务时我们才可以执行否则就需要挂起等待直到有任务生成才去获取任务
  3. 线程执行例程需要设置为静态方法原因如下
  • 使用pthread_create函数创建线程时需要为创建的线程传入一个routine执行例程该routine只有一个参数类型为void的参数以及返回类型为void的返回值。因为我们将线程池封装为一个类此时routine函数就包含两个参数第一个参数就是隐含的this指针直接用来创建线程程序是会报错的
  • 静态成员函数属于类而不属于某个对象也就是说静态成员函数是没有隐藏的this指针的因此我们需要将routine设置为静态方法此时routine函数才真正只有一个参数类型为void*的参数。
  • 但是在静态成员函数内部无法调用非静态成员函数而我们需要在routine函数当中调用该类的某些非静态成员函数比如pop。因此我们需要在创建线程时向routine函数传入的当前对象的this指针此时我们就能够通过该this指针在routine函数内部调用非静态成员函数了。
#pragma once

#include <iostream>
#include <vector>
#include <queue>
#include <unistd.h>
#include "thread.hpp"
#include "lockGuard.hpp"
#include "log.hpp"

#define NUM 3

template <class T>
class ThreadPool
{
public:
    pthread_mutex_t *getMutex()
    {
        return &lock;
    }

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

    void waitCond()
    {
        pthread_cond_wait(&cond, &lock);
    }

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

public:
    ThreadPool(int thread_num = NUM) : num_(thread_num)
    {
        pthread_mutex_init(&lock, nullptr);
        pthread_cond_init(&cond, nullptr);

        for (int i = 1; i <= num_; i++)
        {
            threads_.push_back(new Thread(i, routine, this));
        }
    }

    // 生产
    void run()
    {
        for (auto &iter : threads_)
        {
            iter->start();
            // std::cout << iter->name() << "启动成功" << std::endl;
            logMessage(NORMAL, "%s %s", iter->name().c_str(), "启动成功");
        }
    }

    static void *routine(void *args)
    {

        ThreadData *td = (ThreadData *)args;
        ThreadPool<T> *tp = (ThreadPool<T> *)td->args_;

        while (true)
        {
            T task;
            {
                LockGuard lockguard(tp->getMutex());

                while (tp->isEmpty())
                    tp->waitCond();

                task = tp->getTask();
            }

            // 处理任务
            task(td->name_);
        }
    }

    void pushTask(const T &task)
    {
        LockGuard lockguard(&lock);
        task_queue_.push(task);
        pthread_cond_signal(&cond);
    }

    ~ThreadPool()
    {
        for (auto &iter : threads_)
        {
            iter->join();
            delete iter;
        }

        pthread_mutex_destroy(&lock);
        pthread_cond_destroy(&cond);
    }

private:
    std::vector<Thread *> threads_; // 线程组
    int num_;
    std::queue<T> task_queue_; // 任务队列

    pthread_mutex_t lock; // 互斥锁
    pthread_cond_t cond;  // 条件变量
};

lockGuard.hpp

为了代码更加的模块化我们将互斥锁进行一个封装成一个RAII风格的锁创建对象是调用构造函数加锁出作用域调用析构函数解锁

#pragma once

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

class Mutex
{
public:
    Mutex(pthread_mutex_t *mtx) : pmtx_(mtx)
    {
    }

    void lock()
    {
        pthread_mutex_lock(pmtx_);
    }

    void unlock()
    {
        pthread_mutex_unlock(pmtx_);
    }

    ~Mutex()
    {
    }

private:
    pthread_mutex_t *pmtx_;
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t* mtx) : mtx_(mtx)
    {
        mtx_.lock();
    }

    ~LockGuard()
    {
        mtx_.unlock();
    }

private:
    Mutex mtx_;
};

Task.hpp

这是一个加法的计算任务

#pragma once

#include <iostream>
#include <string>
#include <functional>

typedef std::function<int(int, int)> tfunc_t;

class Task
{
public:
    Task()
    {
    }

    Task(int x, int y, tfunc_t func) : x_(x), y_(y), func_(func)
    {
    }

    void operator()(const std::string& name)
    {
        // std::cout << "线程 " << name << " 处理完成, 结果是: " << x_ << "+" << y_ << "=" << func_(x_, y_) << std::endl;
        logMessage(WARNING, "%s处理完成:%d+%d = %d | %s | %d", name.c_str(), x_, y_, func_(x_, y_), __FILE__, __LINE__);
    }

private:
    int x_;
    int y_;
    tfunc_t func_;
};

log.hpp

此处我们在设置一个日志文件完整的日志功能至少: 日志等级 时间 支持用户自定义(日志内容, 文件行文件名)

#pragma once

#include <iostream>
#include <string>
#include <functional>

typedef std::function<int(int, int)> tfunc_t;

class Task
{
public:
    Task()
    {
    }

    Task(int x, int y, tfunc_t func) : x_(x), y_(y), func_(func)
    {
    }

    void operator()(const std::string& name)
    {
        // std::cout << "线程 " << name << " 处理完成, 结果是: " << x_ << "+" << y_ << "=" << func_(x_, y_) << std::endl;
        logMessage(WARNING, "%s处理完成:%d+%d = %d | %s | %d", name.c_str(), x_, y_, func_(x_, y_), __FILE__, __LINE__);
    }

private:
    int x_;
    int y_;
    tfunc_t func_;
};

运行代码后我们就会发现此时就有4个线程其中1个为主线程
在这里插入图片描述
并且我们会发现这3个线程在处理时会呈现出一定的顺序性因为主线程是每秒push一个任务这3个线程只会有一个线程获取到该任务其他线程都会在等待队列中进行等待当该线程处理完任务后就会因为任务队列为空而排到等待队列的最后当主线程再次push一个任务后会唤醒等待队列首部的一个线程这个线程处理完任务后又会排到等待队列的最后因此这3个线程在处理任务时会呈现出一定的顺序性。
在这里插入图片描述

单例模式下线程池实现

单例模式指的就是一个类只能创建一个对象该模式可以保证系统中该类只有一个实例并提供一个访问它的全局访问点该实例被所有程序模块共享。

接下来我们以懒汉模式为例来实现我们的线程池

  1. 首先我们需要将线程池中构造函数设置为私有因为我们不想让他被多次访问同时我们也要防止赋值和拷贝的情况发生我们需要将拷贝构造函数与赋值运算符重载函数设置为私有或者删除
  2. 提供一个指向单例对象的static指针并在程序入口之前先将其初始化为空
  3. 提供一个全局访问点获取单例对象。

通过上述三点就可以将我们的代码做出如下改变

threadPool.hpp

#pragma once

#include <iostream>
#include <vector>
#include <queue>
#include <unistd.h>
#include "thread.hpp"
#include "lockGuard.hpp"
#include "log.hpp"

#define NUM 3

template <class T>
class ThreadPool
{
public:
    pthread_mutex_t *getMutex()
    {
        return &lock;
    }

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

    void waitCond()
    {
        pthread_cond_wait(&cond, &lock);
    }

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

private:
    ThreadPool(int thread_num = NUM) : num_(thread_num)
    {
        pthread_mutex_init(&lock, nullptr);
        pthread_cond_init(&cond, nullptr);

        for (int i = 1; i <= num_; i++)
        {
            threads_.push_back(new Thread(i, routine, this));
        }
    }

    ThreadPool(const ThreadPool<T> &other) = delete;
    const ThreadPool<T> &operator=(const ThreadPool<T> &other) = delete;

public:
    static ThreadPool<T> *getThreadPool(int num = NUM)
    {
        if (thread_ptr == nullptr)
        {
            LockGuard lockguard(&mutex);

            if (thread_ptr == nullptr)
            {
                thread_ptr = new ThreadPool<T>(num);
            }
        }

        return thread_ptr;
    }

    // 生产
    void run()
    {
        for (auto &iter : threads_)
        {
            iter->start();
            // std::cout << iter->name() << "启动成功" << std::endl;
            logMessage(NORMAL, "%s %s", iter->name().c_str(), "启动成功");
        }
    }

    static void *routine(void *args)
    {

        ThreadData *td = (ThreadData *)args;
        ThreadPool<T> *tp = (ThreadPool<T> *)td->args_;

        while (true)
        {
            T task;
            {
                LockGuard lockguard(tp->getMutex());

                while (tp->isEmpty())
                    tp->waitCond();

                task = tp->getTask();
            }

            // 处理任务
            task(td->name_);
        }
    }

    void pushTask(const T &task)
    {
        LockGuard lockguard(&lock);
        task_queue_.push(task);
        pthread_cond_signal(&cond);
    }

    ~ThreadPool()
    {
        for (auto &iter : threads_)
        {
            iter->join();
            delete iter;
        }

        pthread_mutex_destroy(&lock);
        pthread_cond_destroy(&cond);
    }

private:
    std::vector<Thread *> threads_; // 线程组
    int num_;
    std::queue<T> task_queue_; // 任务队列

    pthread_mutex_t lock; // 互斥锁
    pthread_cond_t cond;  // 条件变量

    static ThreadPool<T> *thread_ptr;
    static pthread_mutex_t mutex;
};

template <typename T>
ThreadPool<T> *ThreadPool<T>::thread_ptr = nullptr;

template <typename T>
pthread_mutex_t ThreadPool<T>::mutex = PTHREAD_MUTEX_INITIALIZER;

我们需要注意的是getThreadPool函数在创建对象过程中需要双检查加锁因为简单的在if语句前后进行加锁解锁操作的话后续在获取创建的单例对象操作时就会进行大量无意义的加锁解锁操作我们进行双检查操作以后就会加锁之前在进行一次判断不为空就直接返回就避免了后序无意义的加锁解锁操作

testMain.cc

#include <iostream>
#include <ctime>
#include <unistd.h>
#include "threadPool.hpp"
#include "Task.hpp"
#include "log.hpp"

int main()
{
    srand((unsigned int)time(nullptr) ^ getpid());
    // ThreadPool<Task>* tp = new ThreadPool<Task>();

    //启动线程
    ThreadPool<Task>::getThreadPool()->run();

    //主线程执行任务
    while(true)
    {
        int x = rand() % 100 + 1;
        usleep(1000);
        int y = rand() % 50 + 1;
        Task t(x, y, [](int x, int y)->int{
            return x + y;
        });

        logMessage(DEBUG, "制作任务完成%d+%d=?", x, y);
        // std::cout << "制作任务完成: " << x << "+" << y << "=?" << std::endl;

        //将任务推送到线程池中
       ThreadPool<Task>::getThreadPool()->pushTask(t);

        sleep(1);
    }
    return 0;
}

STL、智能指针和线程安全

STL中的容器是否是线程安全的?

不是。原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响而且对于不同的容器加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶)因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全。

智能指针是否是线程安全的?

  • 对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题
  • 对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证 shared_ptr 能够高效,原子的操作引用计数。

其他常见的各种锁

  • 悲观锁在每次取数据时总是担心数据会被其他线程修改所以会在取数据前先加锁读锁写锁行锁等当其他线程想要访问数据时被阻塞挂起。
  • 乐观锁每次取数据时候总是乐观的认为数据不会被其他线程修改因此不上锁。但是在更新数据前会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式版本号机制和CAS操作。
  • CAS操作当需要更新数据时判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败失败则重试一般是一个自旋的过程即不断重试。
  • 其次还有自旋锁公平锁非公平锁…
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux