多线程~实现一个自己的线程池,以及基于单例模式的线程池

news2024/11/26 4:45:57

目录

1.线程池的概念

2.线程池的实现

3.基于单例模式的线程池

(1).单例模式的概念

(2).基于单例模式的线程池


1.线程池的概念

        池化技术本质上都是为了提高效率。线程池也是同理,提前准备好一些线程,用来随时处理任务(处理完任务又放回池子),就称为线程池。为了提高效率。避免了在处理短时间任务时由于创建与销毁线程影响任务处理效率

        如果没有线程池,比如用户向服务器发起请求的时候,服务器收到请求需要先创建线程(需要耗时),然后再给用户提供服务,这对用户来说就是不必要的时间消耗。因此,可以提前准备好一些线程,服务器收到用户请求的时候马上就可以从线程池拿出一个线程来给用户提供服务。

2.线程池的实现

        基于任务队列实现线程池,在线程池内部有一个任务队列,线程生产任务,将任务放到任务队列中,然后线程池内部的线程竞争式地去任务队列里面拿任务来执行。

(1).大致实现思路

        1.线程池内部有NUM个线程,还有一个任务队列,这个队列是一个临界资源。

        2.在init里面,创建NUM个线程,并且让他们执行Routine函数,这里的tid不需要被保存,最后一直存在线程池中,不需要被主线程wait

        3.Routine方法内部首先执行detach方法将新线程和主线程分离,这样线程运行完毕后,会自动释放pcb。在当前方法中,线程不断while尝试从任务队列里面获取任务。

        先加锁,while判断如果任务队列为空,就在条件变量下等待。如果任务队列不为空,就从队列中取出任务t,然后解锁。(因为这个队列是一个临界资源(之前除了这个队列还有其他资源也是临界资源),访问该队列需要加锁)

        解锁以后,获取到任务,通过任务对象执行任务即可。

        4.注意,这个Routine方法必须是static静态的,在类中要让线程执行类内成员方法,是不可行的;必须让线程执行静态方法。原因:只要是类内的成员方法,都含有一个隐含的参数this(加上void* arg是两个),而传入的Routine函数的参数只能接收一个参数(语法上规定),就是后面的void* arg,所以是不能够传入带两个参数的函数的。(静态函数就没有this指针,最终只会有一个函数)

        5.PushTask函数,是给main函数调用的,所以需要加锁(可能有多个线程往任务队列里面加任务),往任务队列里面加入任务以后,唤醒其他线程。

        6.PopTask函数,是在Routine方法内部给线程池内的线程调用的,对整个队列判空,获取pop获取任务都已经加过锁了,所以不需要加锁,直接从输出型参数输出任务就行。

        7.这个线程池和我前两篇文章讲述的基于BlockQueue实现的多消费者多生产者模型很像。(这里任务队列没有设置上限,同时在类里面加上了创建线程并执行线程的逻辑)

(2).代码

#pragma once

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

namespace ns_threadpool
{
    const int NUM = 5;

    template <class T>
    class ThreadPool
    {
    private:
        int _thread_num;
        std::queue<T> _task_queue; //该成员是一个临界资源

        pthread_mutex_t _mtx;
        pthread_cond_t _cond;  //条件变量,当队列为空时,线程不再继续从队列中取数据,在该条件变量下等嗲

    public:
        void Lock()
        {
            pthread_mutex_lock(&_mtx);
        }
        void Unlock()
        { 	
            pthread_mutex_unlock(&_mtx);
        }
        void Wait()
        {
            pthread_cond_wait(&_cond, &_mtx);
        }
        void Wakeup()
        {
            pthread_cond_signal(&_cond);
        }
        bool IsEmpey()
        {
            return _task_queue.empty();
        }

    public:
        ThreadPool(int num = NUM) : _thread_num(num)
        {
            pthread_mutex_init(&_mtx, nullptr);
            pthread_cond_init(&_cond, nullptr);
        }
        // 在类中要让线程执行类内成员方法,是不可行的
        // 必须让线程执行静态方法
        static void *Rountine(void *args)
        {
            pthread_detach(pthread_self());  //分离线程,线程运行完会自动释放资源
            ThreadPool<T> *tp = (ThreadPool<T> *)args;

            while (true)
            {
                tp->Lock();
                while (tp->IsEmpey())
                {
                    tp->Wait();  //队列空,线程在条件变量下等待
                }
                //该任务队列中一定有任务了
                T t;
                tp->PopTask(&t);  //这里的t是输出型参数
                tp->Unlock();

                t.run();
            }
        }
        void InitThreadPool()
        {
            pthread_t tid;  //里的tid不需要被保存,线程最后一直存在线程池中,不需要被主线程wait
            for (int i = 0; i < _thread_num; i++)
            {
                pthread_create(&tid, nullptr, Rountine, (void *)this);
                //这里将线程池对象this指针传入,这样Routine内部就可以调用线程池对象里面的加锁解锁函数,以及对队列进行操作
            }
        }
        void PushTask(const T &in)
        {
            Lock();
            _task_queue.push(in);
            Unlock();
            Wakeup();
        }
        void PopTask(T *out)
        {
            *out = _task_queue.front();
            _task_queue.pop();
        }
        ~ThreadPool()
        {
            pthread_mutex_destroy(&_mtx);
            pthread_cond_destroy(&_cond);
        }
    };
} // namespace ns_threadpool

(3).测试

        这里使用的Task任务和上一篇的Task一样,给出两个操作数和一个操作符,进行计算。我们创建一个线程池对象,然后调用InitThreadPool方法初始化,接着就往队列中添加Task即可,这里模拟1s随机生成一个任务,添加到线程池中的任务队列中。

int main()
{
    ThreadPool<Task> *tp = new ThreadPool<Task>(5);
    tp->InitThreadPool();
    srand((long long)time(nullptr));
    while(true)
    {
        sleep(1);  //模拟1s产生一个Task
        Task t(rand()%100+1, rand()%50+1, "+-*/%"[rand()%5]);
        tp->PushTask(t);
    }

    return 0;
}

测试结果:


3.基于单例模式的线程池

(1).单例模式的概念

        某些类,只需要有一个对象(实例),就称之为单例。单例模式有很多种,这里我采用了线程安全的懒汉模式,懒汉方式最核心的思想是 "延时加载"(用到对象的时候才需要加载). 从而能够优化服务器的启动速度

(2).基于单例模式的线程池

在前面实现的线程池的基础上做修改,改成单例模式的线程池

        1.首先单例模式的话,线程池对象只能有一个,所以在类里面定义一个ThreadPool类型的对象指针ins,而且必须是static类型的,该类只有一个实例。

        2.将构造函数设置为私有,并且将拷贝构造函数和赋值运算符重载函数禁用(利用c++11特性,加上=delete)

        3.基于线程安全版的懒汉模式实现单例模式,定义一个public的GetInstance方法,用于给用户获取当前类的单例对象,用户调用该方法的时候,外层判空校验当前ins对象是否已经被实例化,如果已经实例化,则直接返回;如果没有实例化,加锁,然后内层判空,如果为空,new创建对象,调用InitThreadPool初始化线程池。

        4.最后不要忘记在类外对ins初始化(debug了半天,血与泪的教训)

如果没有初始化,会有如下错误

 

代码:

#pragma once

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

namespace ns_threadpool
{
    const int NUM = 5;

    template <class T>
    class ThreadPool
    {
    private:
        int _thread_num;
        std::queue<T> _task_queue; // 该成员是一个临界资源

        pthread_mutex_t _mtx;
        pthread_cond_t _cond;      // 条件变量,当队列为空时,线程不再继续从队列中取数据,在该条件变量下等嗲
        static ThreadPool<T> *ins; // 单例模式,一个类只能有一个实例,设置为static

    private:
        // 构造函数是必须要的,单例模式下,将构造函数设置为私有,不允许外部用户调用
        ThreadPool(int num = NUM) : _thread_num(num)
        {
            pthread_mutex_init(&_mtx, nullptr);
            pthread_cond_init(&_cond, nullptr);
        }

        ThreadPool(const ThreadPool<T> *tp) = delete;               // 禁用构造函数
        ThreadPool<T> &operator=(const ThreadPool<T> &tp) = delete; // 禁用拷贝构造函数

    public:
        static ThreadPool<T> *GetInstance()
        {
            static pthread_mutex_t mtx_single = PTHREAD_MUTEX_INITIALIZER;
            if (ins == nullptr) // 如果不空,就没必要去竞争锁,双重校验锁,提高性能,外层校验时不用加锁,因为本身就是为了防止去竞争锁,而且后面具体要进行修改的时候会加锁
            {
                pthread_mutex_lock(&mtx_single);
                if (ins == nullptr) // 此处判断必须加锁,因为ins本身就是互斥资源,访问的时候就需要加锁,而且外层循环是没有加锁的,走到这里可能ins已经被其他线程实例化了,所以这里必须再加锁判断一次
                {
                    ins = new ThreadPool<T>();
                    ins->InitThreadPool();
                    std::cout << "加载线程池对象完成" << std::endl;
                }
                pthread_mutex_unlock(&mtx_single);
            }
            return ins;
        }

        void Lock()
        {
            pthread_mutex_lock(&_mtx);
        }
        void Unlock()
        {
            pthread_mutex_unlock(&_mtx);
        }
        void Wait()
        {
            pthread_cond_wait(&_cond, &_mtx);
        }
        void Wakeup()
        {
            pthread_cond_signal(&_cond);
        }
        bool IsEmpey()
        {
            return _task_queue.empty();
        }

    public:
        // 在类中要让线程执行类内成员方法,是不可行的
        // 必须让线程执行静态方法
        static void *Rountine(void *args)
        {
            pthread_detach(pthread_self()); // 分离线程,线程运行完会自动释放资源
            ThreadPool<T> *tp = (ThreadPool<T> *)args;

            while (true)
            {
                tp->Lock();
                while (tp->IsEmpey())
                {
                    tp->Wait(); // 队列空,线程在条件变量下等待
                }
                // 该任务队列中一定有任务了
                T t;
                tp->PopTask(&t); // 这里的t是输出型参数
                tp->Unlock();

                t.run();
            }
        }
        void InitThreadPool()
        {
            pthread_t tid; // 里的tid不需要被保存,线程最后一直存在线程池中,不需要被主线程wait
            for (int i = 0; i < _thread_num; i++)
            {
                pthread_create(&tid, nullptr, Rountine, (void *)this);
                // 这里将线程池对象this指针传入,这样Routine内部就可以调用线程池对象里面的加锁解锁函数,以及对队列进行操作
            }
        }
        void PushTask(const T &in)
        {
            Lock();
            _task_queue.push(in);
            Unlock();
            Wakeup();
        }
        void PopTask(T *out)
        {
            *out = _task_queue.front();
            _task_queue.pop();
        }
        ~ThreadPool()
        {
            pthread_mutex_destroy(&_mtx);
            pthread_cond_destroy(&_cond);
        }
    };
    template <class T>
    ThreadPool<T> *ThreadPool<T>::ins = nullptr;  //注意不要忘记了在类外对静态成员ins初始化
} // namespace ns_threadpool

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/167864.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Lesson 1. 线性回归模型的一般实现形式

文章目录一、线性回归模型建模准备1. 数据准备2. 模型准备二、线性回归模型训练1. 模型训练的本质&#xff1a;有方向的参数调整1.1 模型训练与模型参数调整1.2 模型评估指标与损失函数1.3 损失函数与参数求解2. 利用最优化方法求解损失函数2.1 损失函数的求解2.2 图形展示损失…

【DevOps实战|基于Jenkins与Gitlab构建企业级持续集成环境系统】(更新中未完成)

目录 一、DevOps简介 二、CI/CD简介 1、代码部署的最基本流程 2、软件开发生命周期 3、持续集成整体流程 三、Git简介 1、GitHub与Gitlab区别 四、基于Jenkins与Gitlab构建持续集成环境系统 1、环境说明 2、安装gitlab 1&#xff09;配置邮件报警 一、DevOps简介 De…

目标检测的新范式:Towards Open World Object Detection

论文题目&#xff1a;Towards Open World Object Detection 1 摘要 人类有一种识别其环境中未知物体实例的自然本能(natural instinct)。当这些未知的实例最终获得相应的知识时&#xff0c;对它们的内在好奇心有助于了解它们。这促使我们提出一种新的计算机视觉问题称为&…

Week 11

洛谷P1796 汤姆斯的天堂梦 题目描述 汤姆斯生活在一个等级为 000 的星球上。那里的环境极其恶劣&#xff0c;每天 121212 小时的工作和成堆的垃圾让人忍无可忍。他向往着等级为 NNN 的星球上天堂般的生活。 有一些航班将人从低等级的星球送上高一级的星球&#xff0c;有时需…

算法第十四期——动态规划(DP)初入门

目录 DP初步:状态转移与递推 最少硬币问题 DP基础 DP的两个特征 DP:记忆化 图解DP求解过程 最经典的DP问题&#xff1a;0/1背包 模板题&#xff1a;小明的背包 DP状态设计 DP状态转移方程&#xff08;重点&#xff09; 代码 空间优化:滚动数组 (1&#xff09;交替滚…

【机组组合】基于Benders分解算法解决混合整数规划问题——机组组合问题(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

【鸟哥杂谈】腾讯云 CentOS8 Linux环境下通过docker安装mysql

忘记过去&#xff0c;超越自己 ❤️ 博客主页 单片机菜鸟哥&#xff0c;一个野生非专业硬件IOT爱好者 ❤️❤️ 本篇创建记录 2023-01-15 ❤️❤️ 本篇更新记录 2023-01-15 ❤️&#x1f389; 欢迎关注 &#x1f50e;点赞 &#x1f44d;收藏 ⭐️留言&#x1f4dd;&#x1f64…

Vite中如何更好的使用TS

TS 是JS的一个类型检查工具&#xff0c;检查我们代码中可能会存在的一些隐形问题&#xff1b;同时可以使我们的编译器具备一些语法提示功能。 如果我们使用create-vue&#xff08;vue3官方脚手架工具&#xff09;创建了项目&#xff0c;该项目基于 Vite 且 TypeScript 已经准备…

SpringBoot+Vue使用easypol出现无法导出Excel表的问题

本篇博文目录1.异常信息2.原因3.解决办法4.详细的SpringBooteasypol前后端分离实现excel导出的步骤1.异常信息 今天在使用easypol导出Excel表的时候,发现能够从后端接口返回数据&#xff0c;但是就是无法导出Excel,控制台输出devicepolicies:1 Uncaught (in promise) error,并且…

2022年度总结 - 明月醉窗台

2022年度总结 - 明月醉窗台 1.2022年成果 - 感谢CSDN提供了自我创作的平台&#xff0c;有多少个夜晚我将其作为心灵的寄托... - 感谢各位小伙伴的抬爱和学习过程中的共同见证&#xff0c;将人生视为一条向上的曲线是明确且坚持不懈的抉择... - 立足当下&#xff0c;不忘脚踏实…

【算法基础】1.5 前缀和与差分

文章目录前缀和题目描述解法讲解二维前缀和题目描述解法讲解差分题目描述解法讲解二维差分题目描述解法讲解前缀和 题目描述 输入一个长度为 n 的整数序列。 接下来再输入 m 个询问&#xff0c;每个询问输入一对 l,r。 对于每个询问&#xff0c;输出原序列中从第 l 个数到第 …

IVD-Net:多模态UNet在MRI中的椎间盘定位和分割

摘要 本文提出了一种多模态磁共振图像中的椎间盘&#xff08;IVD&#xff09;定位和分割体系结构&#xff0c;它扩展了UNet。与单一的图像相比&#xff0c;多模态的数据带来了互补的信息有助于更好的数据表示和判别能力。 在本文中&#xff0c;每种MRI模态的数据都以不同的路…

Waf功能、分类与绕过

一. waf工作原理 Web应用防火墙是通过执行一系列针对HTTP/HTTPS的安全策略来专门为Web应用提供保护的一款产品。 常见的系统攻击分为两类&#xff1a; 一是利用Web服务器的漏洞进行攻击&#xff0c;如DDOS攻击、病毒木马破坏等攻击&#xff1b;二是利用网页自身的安全漏洞进…

【C语言】实现通讯录(详解)

目录 一、需要实现的功能 1.1 类型和结构体的定义 二、拆分代码 2.1 游戏菜单 2.1.1 函数调用 2.1.2 函数体的实现 2.1.2运行效果 2.2 初始化结构体 2.2.1 函数调用 2.2.2 函数体的实现 2.2.3 运行结果 2.3 添加联系人信息 2.3.1 函数调用 2.3.2 函数体的实现 2…

操作系统之光--鸿蒙

鸿蒙是什么&#xff1f;鸿蒙包含Openharmony和harmonyOS。Openharmony是华为向开放原子开源基金会捐赠了鸿蒙开源部分的代码&#xff0c;归属于开放原子开源基金会。HarmonyOS是基于Openharmony的商业发行版本。目前大家华为手机上运行就是它。鸿蒙能做什么&#xff1f;很明显&…

【算法基础】1.6 双指针算法

文章目录双指针思想最长连续不重复子序列数组元素的目标和题目讲解判断子序列双指针思想 双指针算法&#xff0c;就是可以将 n ^ 2 优化到 n。 最长连续不重复子序列 给定一个长度为 n 的整数序列&#xff0c;请找出最长的不包含重复的数的连续区间&#xff0c;输出它的长度…

IO流的学习

文章目录一、File类的使用创建File实例File常用方法总结二、IO流分类读入数据的基本操作步骤&#xff08;第一种&#xff09;步骤&#xff08;第二种&#xff09;写出数据的基本操作步骤读入并写出数据的基本操作&#xff08;复制&#xff09;步骤注意处理流之一&#xff1a;缓…

【Nginx】静态资源部署(下)

文章目录静态资源的缓存处理缓存概述浏览器缓存相关指令expires指令add_header指令Nginx的跨域问题解决同源策略跨域问题跨域问题的案例演示解决方案静态资源防盗链什么是资源盗链Nginx防盗链的实现原理&#xff1a;针对目录进行防盗链静态资源的缓存处理 缓存概述 什么是缓存…

【爪洼岛冒险记】第4站:Java中如何进行控制台输入输出?用Java实现猜数字小游戏;超详细讲解Java中的方法:方法的重载,方法的签名

&#x1f331;博主简介&#xff1a;是瑶瑶子啦&#xff0c;一名大一计科生&#xff0c;目前在努力学习JavaSE。热爱写博客~正在努力成为一个厉害的开发程序媛&#xff01; &#x1f4dc;所属专栏&#xff1a;爪洼岛冒险记【从小白到大佬之路】 ✈往期博文回顾:链接: 【爪洼岛冒…

【文件指针+文件顺序读写操作函数】

1.文件的打开和关闭 1.1 什么是文件指针 2.文件操作函数 2.1 fgetc函数和fputc函数2.2 fgets函数和fputs函数2.3 fscanf函数和fprintf函数2.4 fwrite函数和fread函数 1.文件的打开和关闭 1.1 什么是文件指针&#xff1f; 每个被使用的文件都在内存中开辟了一个相应的文件…