【Linux】线程实例 | 简单线程池

news2024/11/24 23:05:38

今天来写一个简单版本的线程池

1.啥是线程池

池塘,顾名思义,线程池就是一个有很多线程的容器。

我们只需要把任务交到这个线程的池子里面,其就能帮我们多线程执行任务,计算出结果。

与阻塞队列不同的是,线程池中内有一个队列用于任务管理,并帮我们封装了线程创建的工作。我们不再需要在主执行流里面创建线程(创建线程也是有时间消耗的),而是只关注于任务的创建,交给线程池来运行并产生结果就OK了

前面已经学习过阻塞队列了,此时再来写线程池,就没有那么困难了!

本次线程池的设计还会采用单例模式,同一个模板类型的任务,只需要一个线程池即可

1.1 简单复习单例模式

单例模式分为两种设计方式,一个是懒汉,一个是饿汉

  • 懒汉:刚开始先不创建单例,等第一次使用的时候在创建;缺点是第一次获取单例需要等待,优点是程序启动快
  • 饿汉:main函数执行前,就将单例创建起来;缺点是程序启动会比较慢,优点是启动之后获取单例会快

2.代码示例-处理task

2.1 成员变量

因为是线程池,需要在内部创建出线程来运行,所以我们需要一个num来标识需要创建的线程的数量

template <class T>
class ThreadPool{
private:
	bool _isStart;  // 线程池子是否启动
    int _threadNum; // 线程数量
    queue<T> _tq;   // 任务队列
    pthread_mutex_t _mutex;// 锁
    pthread_cond_t _cond;  // 条件变量
    static ThreadPool<T> *instance; // 单例模式需要用到的指针
}

这里我们并不需要弄一个数组来存放已经创建的线程,因为我们并不关心线程的退出信息,也不需要对线程进行管理。在创建好线程之后,直接detach即可

static变量我们需要在类外初始化,因为是模板类型,所以还需要带上template关键字

// 初始化static变量
template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr;

2.2 构造/析构

本次使用的是懒汉模式的单例,提供一个指针作为单例,不开放构造函数

private:
    ThreadPool(int num = DEFALUT_NUM)
        : _threadNum(num),
          _isStart(false)
    {
        assert(num > 0);
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }
    ThreadPool(const ThreadPool<T> &) = delete;//取消拷贝
    void operator=(const ThreadPool<T> &) = delete;//取消赋值

同时,利用delete关键字,禁止拷贝构造和赋值重载;析构依旧保持公有

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

这种情况下,我们还需要有一个static成员函数来获取单例;在之前的单例模式博客中,提到当初实现的懒汉模式是线程不安全的,因为没有对线程进行加锁,避免多个执行流同时获取单例,导致单例对象冲突的问题。

现在学习了linux的加锁操作,就可以避免掉这个bug了

两次nullptr判断

其中关于两次nullptr判断的原因,详见注释

  • 第一个判断是为了保证单例,只要单例存在了,就不再创建单例
  • 第二个判断是保证线程安全,可能会出现线程a在创建单例,线程b在锁中等待的情况;此时如果不进行第二次nullptr判断,线程b从锁中被唤醒后,又会继续执行,多创建了一个单例!
public:
    static ThreadPool<T> *getInstance()
    {
        static pthread_mutex_t mt;//使用static,只会创建一次;避免多次实例化,一个执行流一个锁,失去效果
        pthread_mutex_init(&mt,nullptr);
        if (instance == nullptr) // 第一次判断
        {
            pthread_mutex_lock(&mt);// 加锁,保证只有一个执行流走到这里
            if (instance == nullptr)// 第二次判断是来确认的,避免出现在加锁前,被其他执行流获取过实例了
            {
                instance = new ThreadPool<T>();// 确认是null,创建单例
            }
        }

        pthread_mutex_unlock(&mt);
        pthread_mutex_destroy(&mt);
        return instance;
    }

2.3 启动线程池

有了线程池,接下来要做的就是启动它😁

启动之前,我们需要assert判断一下该线程池是否已经启动了,避免多次启动线程池出现问题。启动完成之后,更新isStart的状态值

    void start()
    {
        assert(!_isStart);//如果开启了,那么就不能执行该函数
        for (int i = 0; i < _threadNum; i++)
        {
            pthread_t temp;
            pthread_create(&temp, nullptr, threadRoutine, this);//把this当参数传入
            usleep(100);
            pthread_detach(temp);//分离线程
        }
        _isStart = true;//标识状态,代表线程池已经启动了
    }

这里还有另外一个函数threadRoutine,这是每一个线程需要执行的函数,其为static函数。这里我们获取到的都是单例的this指针,访问成员都需要通过this指针来访问

static void *threadRoutine(void *args)
{
    ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);//c++强转
    while (1)
    {
        tp->lockQueue();
        while (!tp->haveTask())
        {
            tp->waitForTask();
        }
        // 任务被拿到了线程的上下文中
        T t = tp->pop();
        tp->unlockQueue();

        // 规定每一个封装的task对象都需要有一个run函数
        t.resultPrint(t.run());//运行并打印结果
    }
}

2.4 封装的加锁/解锁/通知操作

这部分操作比较简单,就不多提了。其实就是把已有的函数改个名字,变成无参可直接调用的函数罢了。

private:
    void lockQueue() { pthread_mutex_lock(&_mutex); }
    void unlockQueue() { pthread_mutex_unlock(&_mutex); }
    bool haveTask() { return !_tq.empty(); }
    void waitForTask() { pthread_cond_wait(&_cond, &_mutex); }
    void singalThread() { pthread_cond_signal(&_cond); }
    T pop()
    {
        T temp = _tq.front();
        _tq.pop();
        return temp;
    }

其中pop()函数设置为了私有,因为线程池会自己开始处理任务,所以不需要外部pop


2.5 插入任务

最后就只剩下任务的插入了,插入一个任务后,使用条件变量,唤醒线程池中的一个线程来执行这个任务!

    //往线程池中给任务
    void push(const T &in)
    {
        lockQueue();
        _tq.push(in);//插入任务
        singalThread();//任务插入后,唤醒一个线程来执行
        unlockQueue();
    }

到这里,线程池就大功告成了!

3.测试

本次测试依旧使用了在线程博客中提到过的task.hpp,完整代码详见我的gitee仓库

因为使用了线程池,主执行流只需要来派发任务即可;

#include "threadpool.hpp"
#include "task.hpp"
#include <string>
#include <time.h>

int main()
{
    const string operators = "+/*/%";
    ThreadPool<Task>*tp = ThreadPool<Task>::getInstance();
    tp->start();

    srand((unsigned long)time(nullptr) ^ getpid() ^ pthread_self());
    // 派发任务的线程
    while(1)
    {
        int one = rand()%50;
        int two = rand()%10;
        char oper = operators[rand()%operators.size()];
        cout << "[" << pthread_self() << "] 主线程派发计算任务: " << one << oper << two << "=?" << "\n";
        Task t(one, two, oper);
        tp->push(t);
        sleep(1);
    }
    
}

此时线程池就会帮我们运行,并将结果输出!

[muxue@bt-7274:~/git/linux/code/23-01-18 threadpool]$ ./test
[140202992179008] 主线程派发计算任务: 14/8=?
[140202973767424] 新线程完成计算任务: 14/8=1
[140202992179008] 主线程派发计算任务: 43*2=?
[140202965374720] 新线程完成计算任务: 43*2=86
[140202992179008] 主线程派发计算任务: 10/9=?
[140202956982016] 新线程完成计算任务: 10/9=1
[140202992179008] 主线程派发计算任务: 25*9=?
[140202948589312] 新线程完成计算任务: 25*9=225
[140202992179008] 主线程派发计算任务: 8/0=?
div zero, abort
[140202940196608] 新线程完成计算任务: 8/0=-1
[140202992179008] 主线程派发计算任务: 38%1=?
[140202973767424] 新线程完成计算任务: 38%1=0
[140202992179008] 主线程派发计算任务: 23/7=?
[140202965374720] 新线程完成计算任务: 23/7=3
[140202992179008] 主线程派发计算任务: 4%4=?
[140202956982016] 新线程完成计算任务: 4%4=0
[140202992179008] 主线程派发计算任务: 44*8=?
[140202948589312] 新线程完成计算任务: 44*8=352
[140202992179008] 主线程派发计算任务: 4/2=?

3.1 修改轻量级进程的名字

Linux提供了一个有趣的接口,可以允许我们修改轻量级进程的名字;

没有修改的时候,默认的名字都是该进程的可执行程序的名字

[muxue@bt-7274:~/git/linux/code/23-01-18 threadpool]$ ps -aL
  PID   LWP TTY          TIME CMD
 6592  6592 pts/7    00:00:00 test
 6592  6593 pts/7    00:00:00 test
 6592  6594 pts/7    00:00:00 test
 6592  6595 pts/7    00:00:00 test
 6592  6596 pts/7    00:00:00 test
 6592  6597 pts/7    00:00:00 test
 6730  6730 pts/8    00:00:00 ps

我们使用prctl接口,修改名字;这个接口的作用是对一个进程进行操作。

#include <sys/prctl.h>
int prctl(int option, unsigned long arg2, unsigned long arg3,
		unsigned long arg4, unsigned long arg5);

其中修改线程名字的操作如下

prctl(PR_SET_NAME, "handler");//修改线程名字为handler

分别修改主执行流和线程池中线程的名字,即可获得不一样的结果

[muxue@bt-7274:~/git/linux/code/23-01-18 threadpool]$ ps -aL
  PID   LWP TTY          TIME CMD
 7793  7793 pts/7    00:00:00 master
 7793  7794 pts/7    00:00:00 handler
 7793  7795 pts/7    00:00:00 handler
 7793  7796 pts/7    00:00:00 handler
 7793  7797 pts/7    00:00:00 handler
 7793  7798 pts/7    00:00:00 handler
 7828  7828 pts/8    00:00:00 ps

这样可以用于标识线程的属性,还是有些用的!

The end

本篇博客到这里就over啦,有啥问题欢迎评论区提出哦!

QQ图片20220413084241

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

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

相关文章

代码随想录刷题-数组-螺旋矩阵II

文章目录螺旋矩阵 II习题我的解法别人的解法螺旋矩阵 II 本节对应代码随想录中&#xff1a;代码随想录&#xff0c;讲解视频&#xff1a;一入循环深似海 | LeetCode&#xff1a;59.螺旋矩阵II_哔哩哔哩_bilibili 习题 题目链接&#xff1a;59. 螺旋矩阵 II - 力扣&#xff0…

将spring boot项目打包成一个可执行的jar包

将spring boot项目打包成一个可执行的jar包&#xff0c;jar包自带了整个运行环境简化配置&#xff0c;可直接运行&#xff0c;本次使用HelloWorld项目演示。 创建Spring Boot项目在pom.xml中导入插件 <build><plugins><plugin><groupId>org.springfr…

计算机网络:移动IP

移动IP相关概念 移动IP技术是移动结点&#xff08;计算机/服务器&#xff09;以固体的网络IP地址&#xff0c;实现跨越不同网段的漫游功能&#xff0c;并保证了基于网络IP的网络权限在漫游中不发生任何改变。移动结点&#xff1a;具有永久IP地址的设备。归属代理&#xff08;本…

西瓜播放器全屏功能源码分析

H5业务中使用了西瓜播放器&#xff0c;嵌入各个APP&#xff0c;全屏时候会有相应的差异。带着好奇心&#xff0c;阅读一下xgpplayer全屏功能部分源代码。源码地址一、工具方法其他方法看名称就知道是dom操作相关&#xff0c;无需多说&#xff0c;上面这两个工具方法主要用来检测…

数学建模资料整理

数学建模中有三类团队&#xff1a; 第一类&#xff1a;拿到题目&#xff0c;讨论&#xff0c;然后建模手开始建模&#xff0c;编程手开始处理数据&#xff0c;写作手开始写作。 第二类&#xff1a;拿到题目&#xff0c;团内大佬&#xff0c;开始建模&#xff0c;然后编程&#…

【GAOPS055】verilog 乘法、除法和取余

乘法硬件原理结论思路1思路2举例编码仿真综合除法硬件原理verilog代码仿真结果资源占用乘法硬件原理 结论 可以将乘法A x B转为A的移位相加。 利用乘2n就是左移n位的特性乘2^n就是左移n位的特性乘2n就是左移n位的特性&#xff0c;将数拆分为2n2^n2n表示 思路1 原始列竖式计…

【C++】通过priority_queue、reverse_iterator加深对于适配器和仿函数的理解

苦尽甘来 文章目录一、仿函数&#xff08;仿函数就是一个封装()运算符重载的类&#xff09;1.C语言的函数指针2.C的仿函数对象二、priority_queue中的仿函数1.模拟实现优先级队列1.1 优先级队列的本质&#xff08;底层容器为vector的适配器&#xff09;1.2 向下调整算法建堆1.3…

git添加子模块(submodule)

git添加子模块(submodule) 背景 有时候自己的项目需要用到别人的开源代码&#xff0c;例如 freertos 和 tinyusb 这个时候有两种选择 将开源的代码下载下来放到自己的 git 中管理 缺点&#xff1a;如果远端仓库更新&#xff0c;自己仓库的代码不会更新 将开源代码通过子模块…

2023河北沃克HEGERLS甘肃金昌重型仓储项目案例|托盘式四向穿梭车智能密集存储系统在工业行业的创新应用

项目名称&#xff1a;自动化仓储托盘式四向穿梭车智能密集立体库项目 项目合作客户&#xff1a;甘肃省金昌市某集团企业 项目施工地域&#xff1a;甘肃省金昌市 设计与承建单位&#xff1a;河北沃克金属制品有限公司&#xff08;自主品牌&#xff1a;海格里斯HEGERLS&#x…

Mysql的Explain关键字

引入 MySQL为我们提供了 explain 关键字来直观的查看一条SQL的执行计划 概念 1.id SELECT识别符&#xff0c;这是SELECT的查询序列号 2.select_type 查询类型 SIMPLE:简单SELECT(不使用UNION或子查询) PRIMARY:最外面的SELECT UNION:UNION中的第二个或后面的SELECT语句 DEPEND…

你的游戏帐号是如何被盗的

据报道&#xff0c;2022上半年&#xff0c;中国游戏市场用户规模达到了5.54亿人&#xff0c;游戏市场销售收入1163.1亿元&#xff0c;相较去年均为同比增长的情况。如此庞大的市场规模&#xff0c;黑色产业链是绕不开的话题。 但相较于游戏中大家常见的玩家与玩家、玩家与官方…

一维连续型随机变量函数的分布例题(一)

设随机变量X的概率密度为&#xff0c;求Y2X8的概率密度。 令g(x)Y&#xff0c;即g(x)2X8。我们可以得到Y的值域为(8,16)。 方法一&#xff1a;看看Y是不是单调可导的函数 此处Y单调可导。 然后求Y的反函数&#xff0c;即。再对h(x)求导可得。 再由公式我们可得 再补上定义域…

[洛谷-P1273]有线电视网(树形DP + 分组背包DP)

[洛谷-P1273]有线电视网&#xff08;树形DP&#xff09;一、题目内容题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1提示二、思路分析三、代码一、题目内容 题目描述 某收费有线电视网计划转播一场重要的足球比赛。他们的转播网和用户终端构成一棵树状结构&#xff…

Nacos配置拉取及配置动态刷新原理【源码阅读】

Nacos配置拉取及配置刷新原理 一、初始化时获取配置文件 背景 SpringCloud项目中SpringBoot在启动阶段除了会创建SpringBoot容器&#xff0c;还会通过bootstrap.yml构建一个SpringCloud容器&#xff0c;之后会在准备上下文阶段通过SPI加载实现类后&#xff0c;会进行配置合并…

Linux(传输层二)

文章目录0. 前言1. TCP协议1-1 TCP协议段格式1. TCP如何解包&#xff1f;2. TCP协议如何交付&#xff08;应用层- - 客户&#xff09;&#xff1f;3. 如何理解报文本身&#xff1f;4. 如何理解报文字段&#xff1f;1-2 确认应答(ACK)机制1-3 超时重传机制1-4 连接管理机制1. TC…

Kafka中那些巧妙的设计

一、kafka的架构 Kafka是一个分布式、多分区、基于发布/订阅模式的消息队列&#xff08;Message Queue&#xff09;&#xff0c;具有可扩展和高吞吐率的特点。 kafka中大致包含以下部分&#xff1a; Producer&#xff1a; 消息生产者&#xff0c;向 Kafka Broker 发消息的客户…

Vue3+TS项目中element-plus自动导入组件后,找不到文件

问题 原因 从报错代码来看&#xff0c;这是一个ts错误&#xff0c;而且是找不到名称 是没有将*.d.ts文件加入到tsconfig.json配置文件中&#xff0c;所以Typescript还不认识它们 解决 //找到项目根目录下 tsconfig.json配置文件 {"include": ["src/**/*.ts…

OpenAI-J 如何进行测试

当你检出 OpenAI-J 项目以后&#xff0c;你可以对 OpenAI-J 进行测试。在测试之前你首先需要获得 OpenAI 的 API Key。OpenAI 的 Key通常是以 sk 开头的字符串。最简单粗暴的办法就是把获得的 key 替换掉上面的字符串&#xff0c;然后进行测试就可以了。运行 Unit 测试在我们的…

Win11的两个实用技巧系列之无法联网怎么办、耳机没声音的多种解决办法

Win11无法联网怎么办? win11安装后设备不能上网的解决办法Win11无法联网怎么办&#xff1f;电脑安装win11系统以后&#xff0c;发现不能上网&#xff0c;连接不上网络&#xff0c;该怎么办呢&#xff1f;下面我们就来看看win11安装后设备不能上网的解决办法Win11安装后&#x…

初级篇 3 - HTML 或 CSS 文件中不懂的标签属性详解

目录一、遇到的不懂的标签属性详解1、meta 标签的 http-equiv 属性(元标签)二、遇到的 CSS 不懂的属性详解vertical-align三、如何规避 HTML 自动换行 - 脱离文档流配置属性 display: inline-block理解 inline、inline-block、blockinline总结&#xff1a;四、导航栏自动弹出子…