Linux:线程 【线程池】

(一)线程池概念

  • 所谓的 线程池 就是 提前创建一批线程,当任务来临时,线程直接从任务队列中获取任务执行,可以提高整体效率;同时一批线程会被合理维护,避免调度时造成额外开销。(创造线程也是需要资源的)。
  • 像这种把未来会高频使用到,并且创建较为麻烦的资源提前申请好的技术称为 池化技术,池化技术 可以极大地提高性能。
  • 池化技术 的本质:空间换时间
    在这里插入图片描述

线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着
监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

1、线程池优点

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

2、线程池的应用场景

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

(二)线程池的模拟实现

1、普通模式线程池

线程池类

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

具体代码如下:

#include <vector>
#include <queue>
#include <iostream>
#include <string>

#define NUM 5

// 描述一个线程信息的结构体
struct thread_data
{
    pthread_t tid;          // 线程 id
    std::string threadname; // 线程的名字,这里是自己定义的
};

template <class T>
class ThreadPool
{
private:
    std::vector<thread_data> _threads; // 存放线程的数组
    std::queue<T> _task;               // 任务队列

    pthread_mutex_t _mutex; // 互斥锁
    pthread_cond_t _cond;   // 条件变量

public:
    // 加锁
    void Lock()
    {
        pthread_mutex_lock(&_mutex);
    }
    // 解锁
    void Unlock()
    {
        pthread_mutex_unlock(&_mutex);
    }

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

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

    // 判断任务队列是否为空
    bool IsQueueEmpty()
    {
        return _task.empty();
    }

    // 获取线程的名字
    std::string GetThreadName(pthread_t tid)
    {
        // 遍历整个线程数组
        for (const auto &ti : _threads)
        {
            if (ti.tid == tid)
                return ti.threadname;
        }
        return "None";
    }

public:
    // 初始化锁和条件变量
    ThreadPool(int num = NUM)
        : _threads(NUM)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }

    // 销毁锁和条件变量
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

    // 线程执行任务
    static void *Headler(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->ThreadSleep();
            }
            T t = tp->Pop();
            tp->Unlock();

            // 处理任务的过程不加锁
            t.run();  //这个是运行任务,是我自己在任务类中设计的函数, 具体的任务处理效果可以自己设置
            std::cout << "thread name : " << name << " 结果等于 : " << t.GetResult() << std::endl;
        }
    }

    // 创造线程
    void CreatThreads()
    {
        int num = _threads.size();
        for (int i = 0; i < num; i++)
        {
            _threads[i].threadname = "thread-" + std::to_string(i + 1);
            pthread_create(&_threads[i].tid, nullptr, Headler, this);
        }
    }

    //发送任务 到 任务队列
    void Push(const T &in)
    {
        // 发布任务需要加锁,生产者保持互斥
        Lock();

        _task.push(in);

        Wakeup();
        Unlock();
    }

    // 线程拿到任务队列中的任务
    T Pop()
    {
        T t = _task.front();
        _task.pop();
        return t;
    }
};

注意:

  1. 在唤醒线程的时候我们要加上 while 循坏 ,因为线程可能造成伪唤醒。
    如果是一下子唤醒所有线程,那么使得在被唤醒的若干线程中,只有个别线程能拿到任务。
  2. 线程执行例程需要是静态的。
    因为默认的执行例程函数的参数是void * ,而类内函数第一个参数是 this 指针,所以我们需要将该函数设置成 static 的。
    但是会出现一个问题:我们在静态成员函数内部无法调用非静态成员函数,而我们需要在Routine函数当中调用该类的某些非静态成员函数,比如Pop。因此我们在进行创建线程的时候,我们把this指针带上,此时我们就能够通过该this指针在Routine函数内部调用非静态成员函数了。

任务类

为了发送任务,我加上了一个任务类, Task.hpp代码如下:

#pragma once
#include <iostream>
#include <string>

std::string opers = "+-*/%";

enum // 规定错误码
{
    DivZero = 1, // 当 除数为0时
    ModZero,     // 当 %的时候 ,xx % 0时
    Unknown      // 出现错误的符号时
};

class Task
{
private:
    int data1_; //
    int data2_;
    char oper_;

    int exitcode_; // 错误码,判断结果是否合理,默认为0
    int result_;   // 结果

public:
    Task(int x, int y, char op) : data1_(x), data2_(y), oper_(op), result_(0), exitcode_(0)
    {
    }

    ~Task()
    {
    }

    void run()
    {
        switch (oper_)
        {
        case '+':
            result_ = data1_ + data2_;
            break;
        case '-':
            result_ = data1_ - data2_;
            break;
        case '*':
            result_ = data1_ * data2_;
            break;
        case '/':
        {
            if (data2_ == 0)
                exitcode_ = DivZero;
            else
                result_ = data1_ / data2_;
        }
        break;
        case '%':
        {
            if (data2_ == 0)
                exitcode_ = ModZero;
            else
                result_ = data1_ % data2_;
        }
        break;
        default:
            exitcode_ = Unknown;
            break;
        }
    }

    //两数运算 所指向的任务
    std::string GetTask()
    {
        std::string r = std::to_string(data1_);
        r += oper_;
        r += std::to_string(data2_);
        r += "= ";
        return r;
    }

    //两数运算的结果
    std::string GetResult()
    {
        std::string r = std::to_string(result_);
        r += "[code: ";
        r += std::to_string(exitcode_);
        r += "]";

        return r;
    }
};

该类主要是实现两数之间的运算,使用该类时需要我们提供具体的 两个数字 和 运算符。


主函数

接下来准备工作已经完毕,我们看看如何运用线程池,main.cc代码如下:

#pragma Once
#include"Task.hpp"
#include"ThreadPool.hpp"
#include<unistd.h>
#include<ctime>

int main()
{
    srand(time(nullptr));

    //创造线程池
    ThreadPool<Task>* tp = new ThreadPool<Task>(5);
    tp->CreatThreads();
    sleep(1);
    while(true)
    {
        //1. 构建任务
        int x = rand() % 10 + 1;
        usleep(10);
        int y = rand() % 5;
        char op = opers[rand()%opers.size()];

        Task t(x, y, op);
        tp->Push(t);
        //2. 交给线程池处理
        std::cout << "main thread make task: " << t.GetTask() << std::endl;

        sleep(1);
    }
    return 0;
}

运行效果如下:
在这里插入图片描述
因为是多线程并发执行,所以打印起来优点乱,但结果是符合预期的。

2、单例模式线程池

什么是单例模式:

  • 一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
  • 比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

线程池类

关于单例模式就不过多的描述了,我们直接实现一个懒汉模式下的线程池。

#include <vector>
#include <queue>
#include <iostream>
#include <string>

#define NUM 5

// 描述一个线程信息的结构体
struct thread_data
{
    pthread_t tid;          // 线程 id
    std::string threadname; // 线程的名字,这里是自己定义的
};

template <class T>
class ThreadPool
{
private:
    std::vector<thread_data> _threads; // 存放线程的数组
    std::queue<T> _task;               // 任务队列

    pthread_mutex_t _mutex; // 互斥锁 ,保持发送任务和处理任务的互斥
    pthread_cond_t _cond;   // 条件变量

    static ThreadPool<T> *tp_;  // 单例模式, 提供一个静态的线程池
    static pthread_mutex_t lock_;  //  该锁保持 申请线程池的互斥 ,避免申请过多的线程池对象

public:
    // 加锁
    void Lock()
    {
        pthread_mutex_lock(&_mutex);
    }
    // 解锁
    void Unlock()
    {
        pthread_mutex_unlock(&_mutex);
    }

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

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

    // 判断任务队列是否为空
    bool IsQueueEmpty()
    {
        return _task.empty();
    }

    // 获取线程的名字
    std::string GetThreadName(pthread_t tid)
    {
        // 遍历整个线程数组
        for (const auto &ti : _threads)
        {
            if (ti.tid == tid)
                return ti.threadname;
        }
        return "None";
    }

private:
    // 初始化锁和条件变量
    ThreadPool(int num = NUM)
        : _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=(const ThreadPool<T> &) = delete;
public:
    // 线程执行任务
    static void *Headler(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->ThreadSleep();
            }
            T t = tp->Pop();
            tp->Unlock();

            // 处理任务的过程不加锁
            t.run();
            std::cout << "thread name : " << name << " 结果等于 : " << t.GetResult() << std::endl;
        }
    }

    // 创造线程
    void CreatThreads()
    {
        int num = _threads.size();
        for (int i = 0; i < num; i++)
        {
            _threads[i].threadname = "thread-" + std::to_string(i + 1);
            pthread_create(&_threads[i].tid, nullptr, Headler, this);
        }
    }

    //发送任务 到 任务队列
    void Push(const T &in)
    {
        // 发布任务需要加锁,生产者保持互斥
        Lock();

        _task.push(in);

        Wakeup();
        Unlock();
    }

    // 线程拿到任务队列中的任务
    T Pop()
    {
        T t = _task.front();
        _task.pop();
        return t;
    }

    //获取线程池对象
    static ThreadPool<T> *GetInstance()
    {
        if (nullptr == tp_) // 申请锁也需要空间,所以当线程池对象已经存在了,就没必要再申请锁了。
        {
            pthread_mutex_lock(&lock_);
            if (nullptr == tp_)
            {
                std::cout << "线程对象创造成功" << std::endl;
                tp_ = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&lock_);
        }

        return tp_;
    }
};

template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;

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

在原来版本的基础上多加了一个锁和 一个静态的线程池对象指针(饿汉模式)。


主函数

看看如何调用该线程池。

int main()
{
    srand(time(nullptr));

    // 创造线程池
    ThreadPool<Task>::GetInstance()->CreatThreads();
    sleep(1);
    while (true)
    {
        // 1. 构建任务
        int x = rand() % 10 + 1;
        usleep(10);
        int y = rand() % 5;
        char op = opers[rand() % opers.size()];

        Task t(x, y, op);
        ThreadPool<Task>::GetInstance()->Push(t);
        // 2. 交给线程池处理
        std::cout << "main thread make task: " << t.GetTask() << std::endl;

        sleep(1);
    }

    return 0;
}

运行效果如下:
在这里插入图片描述

(三)其它线程相关知识

1、STL线程安全

STL中的容器是否是线程安全的? 不是。
原因:

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

2、智能指针线程安全

C++ 标准提供的智能指针有三种:unique_ptrshared_ptrweak_ptr

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

3、其他常见锁

  • 悲观锁:
    在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:
    每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作
  • CAS操作:
    当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
  • 公平锁:
    一种用于同步多线程或多进程之间访问共享资源的机制,它通过使用互斥锁和相关的调度策略来确保资源的公平分配,以提高系统的性能和稳定性
  • 非公平锁:
    通常使用信号量(Semaphore)或自旋锁(Spinlock)等机制。这些锁机制没有严格的按照请求的顺序来分配锁,而是以更高的性能为目标,允许一些线程或进程在较短时间内多次获取锁资源,从而减少了竞争开销
  • 自旋锁:
    申请锁失败时,线程不会被挂起,而且不断尝试申请锁。
    自旋 本质上就是一个不断 轮询 的过程,即不断尝试申请锁,这种操作是十分消耗 CPU 时间的,因此推荐临界区中的操作时间较短时,使用 自旋锁 以提高效率;操作时间较长时,自旋锁 会严重占用 CPU 时间

自旋锁相关接口:

#include <pthread.h>

pthread_spinlock_t lock; // 自旋锁类型

int pthread_spin_init(pthread_spinlock_t *lock, int pshared); // 初始化自旋锁

int pthread_spin_destroy(pthread_spinlock_t *lock); // 销毁自旋锁

// 自旋锁加锁
int pthread_spin_lock(pthread_spinlock_t *lock); // 失败就不断重试(阻塞式)
int pthread_spin_trylock(pthread_spinlock_t *lock); // 失败就继续向后运行(非阻塞式)

// 自旋锁解锁
int pthread_spin_unlock(pthread_spinlock_t *lock);


4、读者写者问题

  • 在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。
  • 通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。

读者写者模型 321 原则

3 种关系:

  • 读者 vs 读者 :无关系
  • 写者 vs 写者 :互斥
  • 读者 vs 写者 :互斥、同步

2 种角色:读者、写者

1 个交易场所:阻塞队列或其他缓冲区

读写锁 相关接口

#include <pthread.h>

pthread_rwlock_t; // 读写锁类型

// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *__restrict__ __rwlock, const pthread_rwlockattr_t *__restrict__ __attr); 

// 销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *__rwlock) 

 // 读者,加锁
int pthread_rwlock_rdlock(pthread_rwlock_t *__rwlock); // 阻塞式
int pthread_rwlock_tryrdlock(pthread_rwlock_t *__rwlock); // 非阻塞式

// 写者,加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *__rwlock); // 阻塞式 
int pthread_rwlock_trywrlock(pthread_rwlock_t *__rwlock); // 非阻塞式

// 解锁(读者锁、写者锁都可以解)
int pthread_rwlock_unlock(pthread_rwlock_t *__rwlock); 

//设置读写优先
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref); 
/* 
pref 共有 3 种选择 
 
PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况 
 
PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和 
PTHREAD_RWLOCK_PREFER_READER_NP 一致 
 
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁 
*/ 

下面是一个伪代码理解 rwlock 的实现原理
在这里插入图片描述
注意:

  • 读者是多于写者的,所以在申请锁的过程中 写者可能会一直申请不到锁,写者陷入死锁状态,这是读者优先模型的缺点。
  • 若我们想改变这一状态,可以采用写者优先的方式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值