【Linux】线程池实现

news/2024/7/24 4:51:05 标签: linux, 多线程, 单例模式

📗线程池实现(单例模式

  • 1️⃣线程池概念
  • 2️⃣线程池代码样例
  • 3️⃣部分问题与细节
    • 🔸类成员函数参数列表中隐含的this指针
    • 🔸单例模式
    • 🔸一个失误导致的bug
  • 4️⃣调用线程池完成任务

1️⃣线程池概念

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

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

线程池示例:

  1. 创建固定数量线程池,循环从任务队列中获取任务对象,
  2. 获取到任务对象后,执行任务对象中的任务接口

2️⃣线程池代码样例

以下为线程池代码:

#pragma once

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


template<class T>
class ThreadPool
{
    private:
        std::queue<T> _q;//任务队列
        pthread_mutex_t _lock;
        pthread_cond_t _cond;//有任务时提醒线程执行任务
        static ThreadPool<T>* _instance;
        ThreadPool()
        {}   
    public:
        static ThreadPool<T>* getInstance()//单例模式,饿汉模式,静态成员
        {
            static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
            if(nullptr == _instance)//双if提高效率,因为只有第一次获取_instance时才会实例化,后续无需再加锁实例化_instance
            {
                pthread_mutex_lock(&mtx);
                if(nullptr == _instance)
                {
                    _instance = new ThreadPool<T>();
                }
                pthread_mutex_unlock(&mtx);
            }
            return _instance;
        }
        void MutexInit()
        {
            pthread_mutex_init(&_lock, nullptr);
        }
        void MutexLock()
        {
            pthread_mutex_lock(&_lock);
        }
        void MutexUnLock()
        {
            pthread_mutex_unlock(&_lock);
        }
        bool IsEmpty()
        {
            return _q.size() == 0 ? true : false;
        }
        void ThreadWait()
        {
            pthread_cond_wait(&_cond, &_lock);
        }
        void ThreadWakeUp()
        {
            pthread_cond_signal(&_cond);
        }
        void PopTask(T* out)//取任务
        {
            //此处不应加锁,应为取任务的时候是带着锁的,若此时申请锁,会出现死锁现象
            *out = _q.front();
            _q.pop();
        }
        //类内部的成员方法都有隐含的this参数,因此要加上static修饰
        static void* Routine(void* args)
        {
            ThreadPool<T>* tp = (ThreadPool<T>*) args;//接收this指针
            pthread_detach(pthread_self());//线程分离
            while(true)
            {
                tp->MutexLock();//加锁,访问临界区_q
                while(tp->IsEmpty())//任务队列为空,挂起等待
                {
                    tp->ThreadWait();
                }
                //到此处说明有任务
                T t;
                tp->PopTask(&t);
                tp->MutexUnLock();//退出临界区_q
                t();
            }
            return nullptr;
        }
        void ThreadPoolInit(int num)//初始化线程池
        {
            pthread_t p[num];
            for(int i = 0; i < num; i++)
            {
                pthread_create(p + i, nullptr, Routine, this);//将this指针作为参数传入
            }
        }
        void PushTask(const T& in)
        {
            //分配任务
            MutexLock();
            _q.push(in);
            MutexUnLock();
            ThreadWakeUp();//唤醒线程完成任务
        }
        ~ThreadPool()
        {
            pthread_mutex_destroy(&_lock);
        }
};


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

3️⃣部分问题与细节

下面分享一些在编写该单例模式线程池代码遇到的一些问题与细节:

🔸类成员函数参数列表中隐含的this指针

我们在初始化线程池的这部分代码,需要创建若干线程来完成其所需要执行的任务,这些线程的例程函数形式为void *(*start_routine) (void *) ,其参数列表中仅有一个参数void*,而如果将这个例程函数定义成成员函数,会有一个隐含的this指针参数,导致形式不一致,因此需要将该例程函数用static修饰为静态的。
又因为静态成员函数只能访问静态成员变量,故我们需要在创建线程时将this指针通过参数传递给例程函数,这样才能在例程函数中使用this指针访问类中的成员变量。
在这里插入图片描述

🔸单例模式

我们这个线程池设计成了单例模式,并且采用的是饿汉模式,即服务启动后只有在用到线程池这个功能时才会创建对象。而在单例模式创建对象时,由于只有第一次创建对象时对象指针为nullptr,故判断是否要创建对象指针的时候可以在加锁之前再进行一次判断提高效率,而无需每次都要先加锁再判断。
在这里插入图片描述

🔸一个失误导致的bug

在线程取任务的接口设计时,我因为这里需要访问任务队列这个临界区给这个过程加上了锁,但是实际上在调用这个接口的时候其实线程就已经申请加了锁,而且两次申请的为同一把锁,就导致出现了线程在已经持有一把锁的情况下又去申请这把锁,从而产生了死锁。
在这里插入图片描述

4️⃣调用线程池完成任务

任务类:
实现x 与 y 的+ - * / % 五种运算。

#pragma once
#include <iostream>

class Task//x op y = z
{
    private:
        int x;
        int y;
        char op;//+-*/%
    public:
        Task(){}
        Task(int _x, int _y, char _op)
        :x(_x),
        y(_y),
        op(_op)
        {}
        void operator()()
        {
            int z = -1;
            switch (op)
            {
            case /* constant-expression */'+':
                /* code */
                z = x + y;
                break;
            case '-':
                z = x - y;
                break;
            case '*':
                z = x * y;
                break;
            case '/':
                if(0 != y)
                    z = x / y;
                else
                    std::cout << "warning: div zero error" << std::endl;
                break;
            case '%':
                if(0 != y)
                    z = x % y;
                else
                    std::cout << "warning: div zero error" << std::endl;
                break;
            default:
                    std::cout << "unkonwn operator" << std::endl;
                break;
            }
            std::cout << "[" << pthread_self() << "] handler task: " << x << " " << op << " " << y << " = " << z << std::endl;
        }
        ~Task(){}
};

主函数:

#include "thread_pool.hpp"
#include "Task.hpp"
#include <string>
#include <unistd.h>

int main()
{
    srand((unsigned long)time(nullptr));
    ThreadPool<Task>* tp = ThreadPool<Task>::getInstance();
    tp->ThreadPoolInit(5);

    const std::string s = "+-*/%";
    while(true)
    {
        int x = rand() % 50;
        int y = rand() % 50;
        char op = s[rand() % 5];
        Task t(x, y, op);
        tp->PushTask(t);
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
结果如上,左侧为线程池中的线程每隔一秒取出任务并执行,右侧为线程池的情况。


http://www.niftyadmin.cn/n/5323890.html

相关文章

NLP论文阅读记录 - WOS | ROUGE-SEM:使用ROUGE结合语义更好地评估摘要

文章目录 前言0、论文摘要一、Introduction1.1目标问题1.2相关的尝试1.3本文贡献 二.相关工作三.本文方法四 实验效果4.1数据集4.2 对比模型4.3实施细节4.4评估指标4.5 实验结果4.6 细粒度分析 五 总结 前言 ROUGE-SEM: Better evaluation of summarization using ROUGE combin…

树莓派ubuntu22桌面配置(一)

烧录系统至树莓派 下载系统&#xff1a;https://ubuntu.com/download/raspberry-pi 选择合适的版本下载 镜像安装器安装&#xff1a;终端输入&#xff1a; sudo snap install rpi-imager 打开镜像安装器&#xff0c;按照需求选择树莓派版本与要写入的系统还有安装的u盘 方案…

【python、nlp】文本预处理

文本预处理及其作用&#xff1a; 文本语料在输送给模型前一般需要一系列的预处理工作&#xff0c;才能符合模型输入的要求&#xff0c;如&#xff1a;将文本转化成模型需要的张量&#xff0c;规范张量的尺寸等&#xff0c;而且科学的文本预处理环节还将有效指导模型超参数的选…

如何自动化部署和发布系统?

如何自动化部署和发布系统&#xff1f; 自动化部署和发布系统可以帮助开发人员更高效地部署和发布代码&#xff0c;减少手动操作的风险和错误。以下是一些自动化部署和发布系统的基本步骤&#xff1a; 选择合适的工具&#xff1a;选择适合你的项目和团队需求的自动化部署和发…

【并发编程】锁

目录 1、锁的分类 1.1 可重入锁、不可重入锁 1.1.1 定义 1.2 乐观锁、悲观锁 1.2.1 定义 1.3 公平锁、非公平锁 1.3.1 定义 1.4 互斥锁、共享锁 1.4.1 定义 2、synchronized 2.1 类锁,对象锁 2.2 synchronized 优化 2.3 synchronized实现原理 2.4 synchronized的…

高级JavaScript。同步和异步,阻塞和非阻塞

同步阻塞 同步非阻塞 异步阻塞 异步非阻塞 在当什么是同步和异步&#xff0c;阻塞与非阻塞的概念还没弄清楚之前&#xff0c;更别提上面这些组合术语了&#xff0c;只会让你更加困惑。 同步和异步 同步和异步其实指的是&#xff0c;请求发起方对消息结果的获取是主动发起…

c++ 经典服务器开源项目 Tinywebserver学习笔记

learning make me happy---更新中 疑问部分ENGINEInnoDB 存储引擎指定为innoDB的作用的意义&#xff1f; 报错部分fatal error: mysql/mysql.h: No such file or directory&#xff1f;进程结束后还占用大量内存&#xff1f; 知识学习和查漏补缺epoll_create&#xff08;5&…

瑞_Java开发手册_(五)MySQL数据库

文章目录 (一) 建表规约(二) 索引规约(三) SQL 语句(四) ORM 映射附&#xff1a;雪花算法&#xff08;Java&#xff09; &#x1f64a;前言&#xff1a;本文章为瑞_系列专栏之《Java开发手册》的MySQL数据库篇&#xff0c;主要介绍建表规约、索引规约、SQL语句、ORM映射。由于博…