线程池 1-17续

本文详细讲述了如何在C++中实现线程池,包括类内创建线程的技巧、线程池的工作原理、同步机制(如锁和条件变量)、任务队列的管理和封装线程类的重要性。作者讨论了线程池作为一种CP模型的应用以及避免并发问题的方法,还提到了单例模式在设计线程池时的考虑.
摘要由CSDN通过智能技术生成

线程池

调系统调用也是有成本的。比如申请内存,一次多给你一点,间接减少系统调用次数
所有的池化技术本质都是以空间换时间
预先创建线程,等待任务到来,当任务到来直接指派给某个线程

将池化技术是为了我们把CP模型和多线程结合一下,有更深理解
还要解决C++类内创建线程的坑

在这里插入图片描述
矩形表示这是一个线程池,线程池内部有一个任务队列,任务队列背后挂靠了很多很多的线程,这些线程就竞争式的去任务队列中拿任务,外部使用线程池的执行流不断地向线程池PUSH任务,然后就不管了,剩下的就由内部线程池来统一来处理这些任务。黑盒式的。

这个东西最终就是个 CP模型。

只不过我们要把类和线程做一下结合。

先写调用逻辑
在这里插入图片描述

在写线程池

线程池里有多个线程你得管理多个线程,就用vector 保存线程的tid
还可以创建线程的信息类。信息类里包含线程的tid 和 线程名,让vector保存信息类

总得有个队列给外部让外部push任务

这是个CP模型,可能你PUSH任务时,正好有线程从队列拿任务,
另外有任务到来时可能其他线程都在休眠我们就应该叫醒一个线程来处理任务,则需要维护同步互斥,就需要锁和条件变量。
锁实现互斥保护临界资源安全的,条件变量用来对线程做唤醒
在这里插入图片描述

之后需要写线程池对外提供的接口,启动Start,线程池启动就是循环创建多线程。
在构造函数时先不创建多线程放到start创建

在这里插入图片描述
未来启动以后所有线程都跑起来了,每一个线程都应该嗷嗷待哺搬的去检测任务队列里是否有任务,有任务处理,没任务就休眠。

类内创建线程细节

C++类内函数隐含了this指针,导致原生线程入口函数形参不匹配,所以线程创建不成功
在这里插入图片描述
解决,要么把入口函数放在类外,
要么把该方法改成静态成语函数,他就没有this指针,形参就只有void*
此时才可以编过。

线程池也还要对外提供一个PUSH任务接口,这样使用线程池的执行流把任务塞进来。
往队列里放任务,可能有线程正在从队列拿任务,所以要维护互斥,生产函数Push和多线程入口函数用同一把锁就能维护,CC PP CP的互斥关系。
当然我放完任务以后,你并不清楚当前线程池中其他线程在干嘛,有可能所有线程都在休眠,所以要唤醒线程让他帮我处理任务。
比如线程池内部没有任何任务到来,就不应该让所有线程一直while循环,应该让他去休眠,有任务到来再把线程唤醒。意味着线程没有被唤醒时要在条件变量下休眠。就是队列里没任务就要去条件变量下休眠(阻塞队列不是你想消费就能消费,资源条件要就绪)。

在这里插入图片描述

则我们的线程入口函数应该做什么呢?
所有的线程都应该先去检测队列里有没有任务,没有任务就去休眠,那你得先检测,你先检测队列就得先加锁
然后再判断队列为空就让线程去休眠,当它醒来时,线程就要把任务从队列里POP拿出来。
拿到任务之后就要处理这个任务,你把这个任务从队列里拿出来了,这个任务就属于你线程私有的了(你都pop并且保存在你函数内部的变量中了)
,没有人和你抢了你已经拿走了,所以处理这个任务就不能写在加锁和解锁之间,这个任务要是很繁琐你还得等处理完才能解锁,其他线程只能等你处理任务完成后再解锁后再申请锁继续处理任务。
在这里插入图片描述

处理任务的代码就比如说计算任务的代码就由线程池里的线程执行流来执行
在这里插入图片描述
另外处理任务的过程和你线程加锁解锁没关系,你就应该在锁外进行处理
,这样每一个线程先取任务,作为消费者在队列里先消费,消费完后处理任务,在你处理任务期间,你已经解锁了,其他线程就可以再继续加锁,然后在进行取任务,再解锁,解锁完之后再处理任务,所以就能看到很多线程都在处理任务,处理任务的过程就在并发访问了。

不怕伪唤醒吗?
假如队列里只有一个任务,可能一下唤醒很多线程,某一个线程处理了这个任务,下一次生产者生产前被线程池消费者抢到了锁,再pop就会出错。
非常怕,所以判断改为循环而不是if
在这里插入图片描述
可是此时把handlertask搞成静态方法,静态成员方法不能直接访问类内成员函数或成员变量,你只能访问静态属性,静态函数。

那怎么处理呢?
创建线程是直接给线程传递当前对象
在这里插入图片描述
利用当前对象的指针调用成员函数,我们是封装了接口
在这里插入图片描述

在这里插入图片描述
对于Pop来说不用再加锁了,因为使用pop时已经是在加锁的场景下,你再申请锁不就死锁了,Pop也不需要加锁。
或者我就直接不封装Pop,直接用当前对象指针调用队列的front。

在这里插入图片描述

你也可以再用一个队列才存放处理任务的结果,再写一个接口就能从线程池拿回处理的结果

线程池不就是CP模型吗,我们以前的进程池它不也是CP模型吗,只不过进程池拿主进程和子进程每一个都建立了一个管道,管道不就是今天讲的任务队列吗,所以想管道里赛对应整数,让进程去处理,管道自带同步和互斥所以当年并没有维护同步和互斥。

封装线程类

我们原生线程是面向过程的,创建出线程,这个线程就失控了,我想控制这个线程,想获取线程名什么的还不好弄,所以需要封装线程。
因为一切皆对象,所以线程当然也是可以被封装的。

#include <pthread.h>
#include <string>
#include <ctime>
#include <functional>

// typedef void (*callback_t)();
static int defalutnum = 1;

template<class T>
class Thread
{
public:
    using func_t = std::function<void(T)>;  

    static void* Routine(void* args)
    {
        Thread* td = (Thread*) args;
        td->Entery();
        return nullptr;
    }
    Thread(func_t cb,T data):tid_(0),name_(""),start_timestamp_(0),isrunning_(false),cb_(cb),data_(data)
    {}
    void Run()
    {
        name_ = "thread-" + std::to_string(defalutnum++);
        start_timestamp_ = time(nullptr);
        isrunning_ = true;
        pthread_create(&tid_,nullptr,Routine,this);
    }
    void Join()
    {
        pthread_join(tid_,nullptr);
        isrunning_ = false;
    }
    void Entery()
    {
        cb_(data_);
    }
    std::string Name()
    {
        return name_;
    }
    uint64_t StartTimestamp()
    {
        return start_timestamp_;
    }
    bool IsRunning()
    {
        return isrunning_;
    }
    ~Thread()
    {}
private:
    pthread_t tid_;
    std::string name_;
    uint64_t start_timestamp_;
    bool isrunning_;
    T data_;
    func_t cb_;
};

线程类中的名字里的num是否有并发问题?
依次创建线程的时候就不会出现问题。

解决线程执行函数传参问题。
大概写了一下

回调函数可以让你传入不同的函数去调用,相较于普通的函数调用更为灵活
回顾C语言https://gitee.com/bitpg/class108/blob/master/2023-01-05-%E6%9D%BF%E4%B9%A6.png

封装线程对象的意义在于,可以放进Vector管理起来
在这里插入图片描述
试一试在线程池里把我们的封装线程用起来。刚好线程池里有一个Vector,将来线程池里不用出现原生线程,而是换成封装线程。

----------------

单例模式设计线程池涉及的加锁双if问题 饿汉 懒汉

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值