【Linux:线程池】

文章目录

  • 1 线程池概念
  • 2 第一个版本的线程池
  • 3 第二个版本的线程池
  • 4 第三个版本的线程池
  • 5 STL中的容器以及智能指针的线程安全问题
  • 6 其他常见的各种锁
  • 7 读者写者问题(了解)


1 线程池概念

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

线程池的应用场景:

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

线程池示例:

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

2 第一个版本的线程池

在创建线程池之前我们想想线程池的成员变量应该有哪些?首先我们需要一个容器来存放线程,所以不妨使用vector;还要使用一个整形变量来记录线程池中线程的个数;为了保证线程安全问题我们还得需要一把锁,同时为了维护同步关系我们还得需要一个条件变量(这里的同步关系是指当没有任务时线程库中的线程就休眠,当有任务时就执行任务);另外我们还得需要一个任务队列。这里我们再封装一个任务类让等会儿验证时效果更加明显。

Task.hpp:

#pragma once
#include <iostream>
using namespace std;

class Task
{
public:
    Task(int x=0, int y=0, char op='+')
        : _x(x), _y(y), _op(op)
    {
    }
    void run()
    {
        switch (_op)
        {
        case '+':
            _res = _x + _y;
            break;
        case '-':
            _res = _x - _y;
            break;
        case '*':
            _res = _x * _y;
            break;
        case '/': 
            if(_y==0)
            {
                _exitCode=1;
                return;
            }
            _res = _x / _y;
            break;
            case '%':
            _res = _x % _y;
            break;
        }
    }

    void formatMsk()
    {
        cout<<"mask:"<<_x<<_op<<_y<<"==?"<<endl;
    }

    void formatRes()
    {
        cout<<"res:"<<_x<<_op<<_y<<"=="<<_res<<endl;
    }

private:
    int _x;
    int _y;
    char _op;
    int _res = 0;
    int _exitCode = 0;
};

现在我们来实现第一个版本:

#pragma once 
#include<iostream>
#include<vector>
#include<queue>
using namespace std;

const int N=5;
template<class T>
class threadPool
{
public:
    threadPool(int sz=N)
    :_sz(sz)
    ,_threads(sz)
    {
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_cond,nullptr);
    }


    static void* Routine(void* args)//用内存池的多线程执行任务
    {
        pthread_detach(pthread_self());//先让自己与主线程分离
        threadPool<T> *ptp=static_cast<threadPool<T> *>(args);

        while(true)
        {
            pthread_mutex_lock(&(ptp->_mutex));

            while((ptp->_masks).empty())
            {
                pthread_cond_wait(&(ptp->_cond),&(ptp->_mutex));
            }

            T task=(ptp->_masks).front();
            (ptp->_masks).pop();
            pthread_mutex_unlock(&(ptp->_mutex));

            task.run();//在临界区外执行任务
            task.formatRes();
        }

        return nullptr;
    }

    void Start()
    {
        for(int i=0;i<_sz;++i)
        {
            pthread_create(&_threads[i],nullptr,Routine,this);
        }
    }

    void PushTask(const T& task)
    {
        pthread_mutex_lock(&_mutex);
        _masks.push(task);
        pthread_mutex_unlock(&_mutex);
        pthread_cond_signal(&_cond);//记得唤醒休眠的线程去执行任务
    }

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

    vector<pthread_t> _threads;
    queue<T> _masks;
    int _sz;//线程池中线程的个数
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;
};

这里面有几个特别需要注意的点:
在这里插入图片描述

  1. Routine我们实现的是static版本的,因为创建线程所要求的函数指针与类内成员函数不吻合,类内成员函数有this指针。
  2. 创建了线程后让该线程与主线程分离,也就是主线程不管新线程的资源回收了。
  3. 执行任务时要在临界区外执行,这样并发执行的效率才会更加高效。
  4. 为了方便使用将类中成员变量都搞成了公有,建议不要这样搞,可以自己写一个get。

至于其他的方面都很简单,相信大家能够很容易理解。
测试程序:

const char *ops = "+-*/%";

int main()
{
    threadPool<Task> *threads = new threadPool<Task>(30);
    threads->Start();
    srand((size_t)time(nullptr));
    while (true)
    {
        int x = rand() % 30 + 1;
        int y = rand() % 30 + 1;
        char op = ops[rand() % strlen(ops)];

        Task t(x, y, op);
        threads->PushTask(t);
        t.formatMsk();
        sleep(1);
    }

    return 0;
}

我们来运行下结果:
在这里插入图片描述


3 第二个版本的线程池

其实第二个版本的线程池与第一个的核心思路基本一致,主要是第二个版本使用的是我们自己模拟实现的创建线程的类,比如我们之前自己模拟实现(本质是封装了库中的线程库接口)的一份Thread.hpp:

#pragma once
#include <iostream>
#include <functional>
using namespace std;

class threadProcess
{
public:
    enum stu
    {
        NEW,
        RUNNING,
        EXIT
    };

    template<class T>
    threadProcess(int num, T exe, void *args)
        : _tid(0)
        , _status(NEW)
        ,_exe(exe)
        , _args(args)
    {
        char name[26];
        snprintf(name, 26, "thread%d", num);
        _name = name;
    }

    static void* runHelper(void *args)
    {
        threadProcess *ts = (threadProcess *)args; 
        
        (*ts)();
        return nullptr;
    }

    void operator()() // 仿函数
    {
        if (_exe != nullptr)
            _exe(_args);
    }

    void Run()
    {
        int n = pthread_create(&_tid, nullptr, runHelper, this);
        if (n != 0)
            exit(-1);
        _status = RUNNING;
    }

    void Join()
    {
        int n = pthread_join(_tid, nullptr);
        if (n != 0)
            exit(-1);
        _status = EXIT;
    }


    string _name;
    pthread_t _tid;
    stu _status;
    function<void*(void*)> _exe;
    void *_args;
};

这样我们自己就能够用自己的线程库来完成了:

#pragma once 
#include"Thread.hpp"
#include<iostream>
#include<vector>
#include<queue>
using namespace std;

const int N=5;
template<class T>
class threadPool
{
public:
    threadPool(int sz=N)
        :_sz(sz)
    {
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_cond,nullptr);
    }


    static void* Routine(void* args)//用内存池的多线程执行任务
    {
        //pthread_detach(pthread_self());调用自己写的线程接口不用在分离了,析构时在join掉就好了
        threadPool<T> *ptp=static_cast<threadPool<T> *>(args);

        while(true)
        {
            pthread_mutex_lock(&(ptp->_mutex));

            while((ptp->_masks).empty())
            {
                pthread_cond_wait(&(ptp->_cond),&(ptp->_mutex));
            }

            T task=(ptp->_masks).front();
            (ptp->_masks).pop();
            pthread_mutex_unlock(&(ptp->_mutex));

            task.run();//在临界区外执行任务
            task.formatRes();

        }

        return nullptr;

    }

    void Init()
    {
        for(int i=0;i<_sz;++i)
        {
           _threads.push_back(threadProcess(i+1,Routine,this));
        }
    }

    void Start()
    {
        for(auto& e:_threads)
        {
            e.Run();
        }
    }

    void PushTask(const T& task)
    {
        
        pthread_mutex_lock(&_mutex);
        _masks.push(task);
        pthread_mutex_unlock(&_mutex);
        pthread_cond_signal(&_cond);//记得唤醒休眠的线程去执行任务
    }

    ~threadPool()
    {
        for(auto& e:_threads)
        {
            e.Join();
        }
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

    void Check()
    {
        for(auto& e:_threads)
        {
            cout<<"name:"<<e._name<<" id"<<e._tid<<endl;
        }
    }

    vector<threadProcess> _threads;
    queue<T> _masks;
    int _sz;//线程池中线程的个数
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;
};

测试代码:

int main()
{
    threadPool<Task> *threads = new threadPool<Task>(8);
    threads->Init();
    threads->Start();
    srand((size_t)time(nullptr));
    while (true)
    {
        int x = rand() % 30 + 1;
        int y = rand() % 30 + 1;
        char op = ops[rand() % strlen(ops)];

        Task t(x, y, op);
        threads->PushTask(t);
        t.formatMsk();
        sleep(1);
    }
    return 0;
}

运行结果:
在这里插入图片描述


4 第三个版本的线程池

这个版本的线程池在前面版本的基础上加了一个单例模式。因为我们发现其实线程池只需要一个就可以了,我们使用懒汉模式来创建单例。

代码实现:

#pragma once
#include "Thread.hpp"
#include <iostream>
#include <vector>
#include <queue>
using namespace std;

const int N = 5;
template <class T>
class threadPool
{
public:

    static threadPool<T>* GetInstance(int sz=N)
    {
        if(_sta_obj==nullptr)
        {
            pthread_mutex_lock(&_mutex);
            if(_sta_obj==nullptr)
            {
                _sta_obj=new threadPool<T>(sz);
            }
            pthread_mutex_unlock(&_mutex);
        }
    }

    static void *Routine(void *args) // 用内存池的多线程执行任务
    {
        // pthread_detach(pthread_self());调用自己写的线程接口不用在分离了,析构时在join掉就好了
        threadPool<T> *ptp = static_cast<threadPool<T> *>(args);

        while (true)
        {
            pthread_mutex_lock(&(ptp->_mutex));

            while ((ptp->_masks).empty())
            {
                pthread_cond_wait(&(ptp->_cond), &(ptp->_mutex));
            }

            T task = (ptp->_masks).front();
            (ptp->_masks).pop();
            pthread_mutex_unlock(&(ptp->_mutex));

            task.run(); // 在临界区外执行任务
            task.formatRes();
        }

        return nullptr;
    }

    void Init()
    {
        for (int i = 0; i < _sz; ++i)
        {
            _threads.push_back(threadProcess(i + 1, Routine, this));
        }
    }

    void Start()
    {
        for (auto &e : _threads)
        {
            e.Run();
        }
    }

    void PushTask(const T &task)
    {

        pthread_mutex_lock(&_mutex);
        _masks.push(task);
        pthread_mutex_unlock(&_mutex);
        pthread_cond_signal(&_cond); // 记得唤醒休眠的线程去执行任务
    }

    ~threadPool()
    {
        for (auto &e : _threads)
        {
            e.Join();
        }
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

    void Check()
    {
        for (auto &e : _threads)
        {
            cout << "name:" << e._name << " id" << e._tid << endl;
        }
    }

    vector<threadProcess> _threads;
    queue<T> _masks;
    int _sz; // 线程池中线程的个数
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;

private:
    threadPool(int sz = N)
        : _sz(sz)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }

    threadPool(const threadPool<T>& th)=delete;
    threadPool<T>& operator=(const threadPool<T>& th)=delete;

    static threadPool<T>* _sta_obj;
};
template<class T>
threadPool<T>* threadPool<T>::_sta_obj=nullptr;

其中注意点:
在这里插入图片描述
在加锁时为了高效我们是用了双重if条件判断

在这里插入图片描述
注意将构造函数搞成了私有,拷贝构造和拷贝赋值都删掉了。


5 STL中的容器以及智能指针的线程安全问题

STL中的容器是否是线程安全的?

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

智能指针是否是线程安全的?

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


6 其他常见的各种锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
  • 自旋锁,公平锁,非公平锁。

7 读者写者问题(了解)

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

读写锁的行为:

当前锁的状态读锁请求写锁请求
无锁可以可以
读锁可以阻塞
写锁阻塞阻塞
  • 注意:写独占,读共享,读锁优先级高

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【绪论0】

#pic_center R 1 R_1 R1​ R 2 R^2 R2 目录 知识框架No.0 引言No.1 操作系统的概念功能和定义一、操作系统的概念和定义1、电脑的演变 二、操作系统的功能和目标 No.2 操作系统的特征一、并发二、共享三、虚拟四、异步 No.3 操作系统的发展与分类一、手工操作阶段二、批处理阶段…

elementui Cascader 级联选择使用心得

相信大家对于elementui并不陌生&#xff0c;作为适配Vue的优秀UI框架之一&#xff0c;一直被所有的开发者痛并快乐着。今天要记录的就是里边的主角之一Cascader。 首先先介绍一下Cascader ---> 当一个数据集合有清晰的层级结构时&#xff0c;可通过级联选择器逐级查看并选择…

IDEA超强XSD文件编辑插件-XSD / WSDL Visualizer

前言 XSD / WSDL Visualizer可以简化XML架构定义(XSD)和WSDL文件编辑过程; 通过使用与IntelliJ无缝集成的可视化编辑器&#xff0c;转换处理XSD和WSDL文件的方式。告别导航复杂和难以阅读的代码的挫败感&#xff0c;迎接流线型和直观的体验。 插件安装 在线安装 IntelliJ IDE…

6.物联网操作系统信号量

一。信号量的概念与应用 信号量定义 FreeRTOS信号量介绍 FreeRTOS信号量工作原理 1.信号量的定义 多任务环境下使用&#xff0c;用来协调多个任务正确合理使用临界资源。 2.FreeRTOS信号量介绍 Semaphore包括Binary&#xff0c;Count&#xff0c;Mutex&#xff1b; Mutex包…

【面试题】位图

文章目录 位图如何添加数据如何删除数据代码实现给100亿个整数&#xff0c;如何找到只出现一次的数字代码实现给两个文件&#xff0c;分别有100亿个整数&#xff0c;但只有1g内存&#xff0c;如何找到文件的交集&#xff1f;1个文件有100亿个int&#xff0c;1G内存&#xff0c;…

EventBus 开源库学习(一)

一、概念 EventBus是一款在 Android 开发中使用的发布-订阅事件总线框架&#xff0c;基于观察者模式&#xff0c;将事件的接收者和发送者解耦&#xff0c;简化了组件之间的通信&#xff0c;使用简单、效率高、体积小。 一句话&#xff1a;用于Android组件间通信的。 二、原理…

插入排序讲解

插入排序&#xff08;Insertion-Sort&#xff09;一般也被称为直接插入排序。对于少量元素的排序&#xff0c;它是一个有效的算法。插入排序是一种最简单的排序方法&#xff0c;它的基本思想是将一个记录插入到已经排好序的有序表中&#xff0c;从而一个新的、记录数增1的有序表…

【Linux命令详解 | pwd命令】Linux系统中用于显示当前工作目录的命令

文章标题 简介一&#xff0c;参数列表二&#xff0c;使用介绍1. pwd命令的基本使用2. pwd命令中的参数3. pwd命令的工作机制4. pwd命令的实际应用 总结 简介 pwd命令是Linux中的基础命令之一&#xff0c;使用该命令可以快速查看当前工作目录。在掌握Linux命令时&#xff0c;pw…

golang函数传参——值传递理解

做了五年的go开发&#xff0c;却并没有什么成长&#xff0c;都停留在了业务层面了。一直以为golang中函数传参&#xff0c;如果传的是引用类型&#xff0c;则是以引用传递&#xff0c;造成这样的误解&#xff0c;实在也不能怪我。我们来看一个例子&#xff0c;众所周知&#xf…

单例模式(C++)

定义 保证一个类仅有一个实例&#xff0c;并提供一个该实例的全局访问点。 应用场景 在软件系统中&#xff0c;经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例&#xff0c;才能确保它们的逻辑正确性、以及良好的效率。如何绕过常规的构造器&#xff0c;提供一种…

微信小程序iconfont真机渲染失败

解决方法&#xff1a; 1.将下载的.woff文件在transfonter转为base64&#xff0c; 2.打开网站&#xff0c;导入文件&#xff0c;开启base64按钮&#xff0c;下载转换后的文件 3. 在下载解压后的文件夹中找到stylesheet.css&#xff0c;并复制其中的base64 4. 修改index.wxss文…

Netty:当Channel将数据发送成功以后,存放发送数据的ByteBuf会被自动释放

说明 使用Netty框架编程&#xff0c;当Channel将数据发送成功以后&#xff0c;存放发送数据的ByteBuf会被自动释放。 为了进行验证&#xff0c;我们可以在发送数据前&#xff0c;通过io.netty.buffer.ByteBuf的refCnt()函数看看引用计数&#xff0c;通过Channel发送成功以后再…

(数据库系统概论|王珊)第一章绪论-第一节:数据库系统概论

目录 一&#xff1a;四大基本概念 &#xff08;1&#xff09;数据(Data) &#xff08;2&#xff09;数据库(DataBase,DB) &#xff08;3&#xff09;数据库管理系统(DataBase Management System,DBMS) &#xff08;4&#xff09;数据库系统(Database System&#xff0c;DBS…

Laravel 框架路由参数.重定向.视图回退.当前路由.单行为 ②

作者 : SYFStrive 博客首页 : HomePage &#x1f4dc;&#xff1a; THINK PHP &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f517; &#x1f4cc;&#xff1a;觉得文章不错可以点点关注 &#x1f44…

Nios初体验之——Hello world!

文章目录 前言一、系统设计1、系统模块框图2、系统涉及到的模块1、时钟2、nios2_qsys3、片内存储&#xff08;onchip_rom、onchip_ram&#xff09;4、串行通信&#xff08;jtag_uart&#xff09;5、System ID&#xff08;sysid_qsys&#xff09; 二、硬件设计1、创建Qsys2、重命…

VGGNet剪枝实战:使用VGGNet训练、稀疏训练、剪枝、微调等,剪枝出只有3M的模型

摘要 本文讲解如何实现VGGNet的剪枝操作。剪枝的原理&#xff1a;在BN层网络中加入稀疏因子&#xff0c;训练使得BN层稀疏化&#xff0c;对稀疏训练的后的模型中所有BN层权重进行统计排序&#xff0c;获取指定保留BN层数量即取得排序后权重阈值thres。遍历模型中的BN层权重&am…

MySQL事务管理

MySQL事务管理 MySQL增删查改时的问题一.什么是事务&#xff1f;二.为什么会出现事务&#xff1f;三.事务的其他属性1. 事务的版本支持2. 事务的提交方式 四.事务的准备工作五.事务的操作1. 事务的正常操作2. 事务的异常验证与产出结论 六.事务的隔离级别1. 事务隔离级别概念2.…

CentOS 安装 Jenkins

本文目录 1. 安装 JDK2. 获取 Jenkins 安装包3. 将安装包上传到服务器4. 修改 Jenkins 配置5. 启动 Jenkins6. 打开浏览器访问7. 获取并输入 admin 账户密码8. 跳过插件安装9. 添加管理员账户 1. 安装 JDK Jenkins 需要依赖 JDK&#xff0c;所以先安装 JDK1.8。输入以下命令&a…

如何打造属于自己的个人IP?

在当今信息爆炸的时代&#xff0c;个人 IP 已经成为人们在网络世界中的独特标签。无论是在职场上、创业中&#xff0c;还是在社交生活中&#xff0c;拥有个人 IP 的人都能脱颖而出&#xff0c;吸引更多的关注和机会。那么&#xff0c;如何打造属于自己的个人 IP 呢&#xff1f;…