Linux之线程池

线程池

  • 线程池概念
  • 线程池的应用场景
  • 线程池实现原理
  • 单例模式下线程池实现
  • STL、智能指针和线程安全
  • 其他常见的各种锁

线程池概念

线程池:一种线程使用模式。

线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。

这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

线程池的应用场景

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

线程池实现原理

线程池通过一个线程安全的阻塞任务队列加上一个或一个以上的线程实现,线程池中的线程可以从阻塞队列中获取任务进行任务处理,当线程都处于繁忙状态时可以将任务加入阻塞队列中,等到其它的线程空闲后进行处理。
在这里插入图片描述

testMain.cc

主线程任务逻辑启动线程,不断向任务队列中push任务就可以了,此时线程接收到任务就会进行处理:

#include <iostream>
#include <ctime>
#include <unistd.h>
#include "threadPool.hpp"
#include "Task.hpp"
#include "log.hpp"int main()
{srand((unsigned int)time(nullptr) ^ getpid());ThreadPool<Task>* tp = new ThreadPool<Task>();//启动线程tp->run();//主线程执行任务while(true){int x = rand() % 100 + 1;usleep(1000);int y = rand() % 50 + 1;Task t(x, y, [](int x, int y)->int{return x + y;});logMessage(DEBUG, "制作任务完成:%d+%d=?", x, y);// std::cout << "制作任务完成: " << x << "+" << y << "=?" << std::endl;//将任务推送到线程池中tp->pushTask(t);sleep(1);}return 0;
}

thread.hpp

我们对创建线程进行封装,包含线程名,线程个数,回调函数,线程ID等;

#pragma once#include <iostream>
#include <string>
#include <functional>
#include <cstdio>typedef void *(*func_t)(void *);class ThreadData
{
public:std::string name_;void *args_;
};class Thread
{
public:Thread(int num, func_t callback, void *args) : func_(callback){char nameBuffer[64];snprintf(nameBuffer, sizeof nameBuffer, "Thread-%d", num);name_ = nameBuffer;tdata_.args_ = args;tdata_.name_ = name_;}void start(){pthread_create(&tid_, nullptr, func_, (void *)&tdata_);}void join(){pthread_join(tid_, nullptr);}std::string name(){return name_;}~Thread(){}private:std::string name_; // 线程名int num_;          // 线程个数func_t func_;      // 回调函数pthread_t tid_;    // 线程IDThreadData tdata_;
};

threadPool.hpp

线程池中我们需要用注意的是:

  1. 需要用到条件变量与互斥锁,因为线程池中的任务队列会被多个执行流访问,所以我们必须引入互斥锁;
  2. 当线程池中任务队列为满时,我们此时push任务就无法push进去,此时就需要挂起等待,直到线程将某一任务执行完毕,唤醒等待队列,才可以继续进行push,我们执行任务也是一样,只有当任务队列中有任务时,我们才可以执行,否则就需要挂起等待,直到有任务生成才去获取任务;
  3. 线程执行例程需要设置为静态方法,原因如下:
  • 使用pthread_create函数创建线程时,需要为创建的线程传入一个routine(执行例程),该routine只有一个参数类型为void的参数,以及返回类型为void的返回值。因为我们将线程池封装为一个类,此时routine函数就包含两个参数,第一个参数就是隐含的this指针,直接用来创建线程程序是会报错的;
  • 静态成员函数属于类,而不属于某个对象,也就是说静态成员函数是没有隐藏的this指针的,因此我们需要将routine设置为静态方法,此时routine函数才真正只有一个参数类型为void*的参数。
  • 但是在静态成员函数内部无法调用非静态成员函数,而我们需要在routine函数当中调用该类的某些非静态成员函数,比如pop。因此我们需要在创建线程时,向routine函数传入的当前对象的this指针,此时我们就能够通过该this指针在routine函数内部调用非静态成员函数了。
#pragma once#include <iostream>
#include <vector>
#include <queue>
#include <unistd.h>
#include "thread.hpp"
#include "lockGuard.hpp"
#include "log.hpp"#define NUM 3template <class T>
class ThreadPool
{
public:pthread_mutex_t *getMutex(){return &lock;}bool isEmpty(){return task_queue_.empty();}void waitCond(){pthread_cond_wait(&cond, &lock);}T getTask(){T t = task_queue_.front();task_queue_.pop();return t;}public:ThreadPool(int thread_num = NUM) : num_(thread_num){pthread_mutex_init(&lock, nullptr);pthread_cond_init(&cond, nullptr);for (int i = 1; i <= num_; i++){threads_.push_back(new Thread(i, routine, this));}}// 生产void run(){for (auto &iter : threads_){iter->start();// std::cout << iter->name() << "启动成功" << std::endl;logMessage(NORMAL, "%s %s", iter->name().c_str(), "启动成功");}}static void *routine(void *args){ThreadData *td = (ThreadData *)args;ThreadPool<T> *tp = (ThreadPool<T> *)td->args_;while (true){T task;{LockGuard lockguard(tp->getMutex());while (tp->isEmpty())tp->waitCond();task = tp->getTask();}// 处理任务task(td->name_);}}void pushTask(const T &task){LockGuard lockguard(&lock);task_queue_.push(task);pthread_cond_signal(&cond);}~ThreadPool(){for (auto &iter : threads_){iter->join();delete iter;}pthread_mutex_destroy(&lock);pthread_cond_destroy(&cond);}private:std::vector<Thread *> threads_; // 线程组int num_;std::queue<T> task_queue_; // 任务队列pthread_mutex_t lock; // 互斥锁pthread_cond_t cond;  // 条件变量
};

lockGuard.hpp

为了代码更加的模块化,我们将互斥锁进行一个封装成一个RAII风格的锁,创建对象是调用构造函数加锁,出作用域调用析构函数解锁:

#pragma once#include <iostream>
#include <pthread.h>class Mutex
{
public:Mutex(pthread_mutex_t *mtx) : pmtx_(mtx){}void lock(){pthread_mutex_lock(pmtx_);}void unlock(){pthread_mutex_unlock(pmtx_);}~Mutex(){}private:pthread_mutex_t *pmtx_;
};class LockGuard
{
public:LockGuard(pthread_mutex_t* mtx) : mtx_(mtx){mtx_.lock();}~LockGuard(){mtx_.unlock();}private:Mutex mtx_;
};

Task.hpp

这是一个加法的计算任务:

#pragma once#include <iostream>
#include <string>
#include <functional>typedef std::function<int(int, int)> tfunc_t;class Task
{
public:Task(){}Task(int x, int y, tfunc_t func) : x_(x), y_(y), func_(func){}void operator()(const std::string& name){// std::cout << "线程 " << name << " 处理完成, 结果是: " << x_ << "+" << y_ << "=" << func_(x_, y_) << std::endl;logMessage(WARNING, "%s处理完成:%d+%d = %d | %s | %d", name.c_str(), x_, y_, func_(x_, y_), __FILE__, __LINE__);}private:int x_;int y_;tfunc_t func_;
};

log.hpp

此处我们在设置一个日志文件,完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名);

#pragma once#include <iostream>
#include <string>
#include <functional>typedef std::function<int(int, int)> tfunc_t;class Task
{
public:Task(){}Task(int x, int y, tfunc_t func) : x_(x), y_(y), func_(func){}void operator()(const std::string& name){// std::cout << "线程 " << name << " 处理完成, 结果是: " << x_ << "+" << y_ << "=" << func_(x_, y_) << std::endl;logMessage(WARNING, "%s处理完成:%d+%d = %d | %s | %d", name.c_str(), x_, y_, func_(x_, y_), __FILE__, __LINE__);}private:int x_;int y_;tfunc_t func_;
};

运行代码后,我们就会发现此时就有4个线程,其中1个为主线程:
在这里插入图片描述
并且我们会发现这3个线程在处理时会呈现出一定的顺序性,因为主线程是每秒push一个任务,这3个线程只会有一个线程获取到该任务,其他线程都会在等待队列中进行等待,当该线程处理完任务后就会因为任务队列为空而排到等待队列的最后,当主线程再次push一个任务后会唤醒等待队列首部的一个线程,这个线程处理完任务后又会排到等待队列的最后,因此这3个线程在处理任务时会呈现出一定的顺序性。
在这里插入图片描述

单例模式下线程池实现

单例模式:指的就是一个类只能创建一个对象,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

接下来我们以懒汉模式为例,来实现我们的线程池:

  1. 首先,我们需要将线程池中构造函数设置为私有,因为我们不想让他被多次访问,同时我们也要防止赋值和拷贝的情况发生,我们需要将拷贝构造函数与赋值运算符重载函数设置为私有或者删除;
  2. 提供一个指向单例对象的static指针,并在程序入口之前先将其初始化为空;
  3. 提供一个全局访问点获取单例对象。

通过上述三点就可以将我们的代码做出如下改变:

threadPool.hpp

#pragma once#include <iostream>
#include <vector>
#include <queue>
#include <unistd.h>
#include "thread.hpp"
#include "lockGuard.hpp"
#include "log.hpp"#define NUM 3template <class T>
class ThreadPool
{
public:pthread_mutex_t *getMutex(){return &lock;}bool isEmpty(){return task_queue_.empty();}void waitCond(){pthread_cond_wait(&cond, &lock);}T getTask(){T t = task_queue_.front();task_queue_.pop();return t;}private:ThreadPool(int thread_num = NUM) : num_(thread_num){pthread_mutex_init(&lock, nullptr);pthread_cond_init(&cond, nullptr);for (int i = 1; i <= num_; i++){threads_.push_back(new Thread(i, routine, this));}}ThreadPool(const ThreadPool<T> &other) = delete;const ThreadPool<T> &operator=(const ThreadPool<T> &other) = delete;public:static ThreadPool<T> *getThreadPool(int num = NUM){if (thread_ptr == nullptr){LockGuard lockguard(&mutex);if (thread_ptr == nullptr){thread_ptr = new ThreadPool<T>(num);}}return thread_ptr;}// 生产void run(){for (auto &iter : threads_){iter->start();// std::cout << iter->name() << "启动成功" << std::endl;logMessage(NORMAL, "%s %s", iter->name().c_str(), "启动成功");}}static void *routine(void *args){ThreadData *td = (ThreadData *)args;ThreadPool<T> *tp = (ThreadPool<T> *)td->args_;while (true){T task;{LockGuard lockguard(tp->getMutex());while (tp->isEmpty())tp->waitCond();task = tp->getTask();}// 处理任务task(td->name_);}}void pushTask(const T &task){LockGuard lockguard(&lock);task_queue_.push(task);pthread_cond_signal(&cond);}~ThreadPool(){for (auto &iter : threads_){iter->join();delete iter;}pthread_mutex_destroy(&lock);pthread_cond_destroy(&cond);}private:std::vector<Thread *> threads_; // 线程组int num_;std::queue<T> task_queue_; // 任务队列pthread_mutex_t lock; // 互斥锁pthread_cond_t cond;  // 条件变量static ThreadPool<T> *thread_ptr;static pthread_mutex_t mutex;
};template <typename T>
ThreadPool<T> *ThreadPool<T>::thread_ptr = nullptr;template <typename T>
pthread_mutex_t ThreadPool<T>::mutex = PTHREAD_MUTEX_INITIALIZER;

我们需要注意的是getThreadPool函数在创建对象过程中需要双检查加锁,因为简单的在if语句前后进行加锁解锁操作的话,后续在获取创建的单例对象操作时就会进行大量无意义的加锁解锁操作,我们进行双检查操作以后,就会加锁之前在进行一次判断,不为空就直接返回,就避免了后序无意义的加锁解锁操作;

testMain.cc

#include <iostream>
#include <ctime>
#include <unistd.h>
#include "threadPool.hpp"
#include "Task.hpp"
#include "log.hpp"int main()
{srand((unsigned int)time(nullptr) ^ getpid());// ThreadPool<Task>* tp = new ThreadPool<Task>();//启动线程ThreadPool<Task>::getThreadPool()->run();//主线程执行任务while(true){int x = rand() % 100 + 1;usleep(1000);int y = rand() % 50 + 1;Task t(x, y, [](int x, int y)->int{return x + y;});logMessage(DEBUG, "制作任务完成:%d+%d=?", x, y);// std::cout << "制作任务完成: " << x << "+" << y << "=?" << std::endl;//将任务推送到线程池中ThreadPool<Task>::getThreadPool()->pushTask(t);sleep(1);}return 0;
}

STL、智能指针和线程安全

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

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

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

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

其他常见的各种锁

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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何在Node.js中使用环境变量或命令行参数来设置HTTP爬虫ip?

首先&#xff0c;定义问题&#xff1a;在 Node.js 应用程序中&#xff0c;我们可以通过环境变量或命令行参数来设置HTTP爬虫ip&#xff0c;以便在发送请求时使用这些HTTP爬虫ip。 亲身经验&#xff1a;我曾经需要为一个项目设置HTTP爬虫ip&#xff0c;以便在发送请求时使用这些…

EtherNet Ip工业RFID读写器与欧姆龙PLC 配置示例说明

一、准备阶段 POE交换机欧姆龙PLC 支持EtherNet Ip协议CX-Programmer 9.5配置软件 二、配置读卡器 1、打开软件 2、选择网卡&#xff0c;如果多网卡的电脑请注意对应所接的网卡&#xff0c;网卡名一般为“Network adapter Realtek PCIe GBE Family” 3、点击“选择网卡”&…

Node学习笔记之user用户API模块

1、获取用户的基本信息 步骤 获取登录会话存储的session中用户的id判断是否获取到id根据用户id查询数据库中的个人信息检查指定 id 的用户是否存在将密码设置为空将数据返回给前端 // 获取用户信息数据 exports.userinfo (req, res) > {(async function () {// 1. 获取…

BUUCTF rar 1

BUUCTF:https://buuoj.cn/challenges 题目描述&#xff1a; 这个是一个rar文件&#xff0c;里面好像隐藏着什么秘密&#xff0c;但是压缩包被加密了&#xff0c;毫无保留的告诉你&#xff0c;rar的密码是4位纯数字。 密文&#xff1a; 下载附件&#xff0c;解压得到一个rar压…

MySQL---JDBC编程

文章目录 什么是JDBC&#xff1f;JDBC的工作原理JDBC的使用添加驱动创建数据源DataSource创建数据库连接Connection创建操作命令Statement执行SQL指令释放资源 通过JDBC演示CRUD新增查询&#xff08;需要增加一步 遍历结果集合的操作&#xff09;修改删除 什么是JDBC&#xff1…

Qt界面实现中英文切换

要实现的效果&#xff0c;是下拉列表切换中文和English实现按钮文本中英文内容切换。 实现步骤&#xff1a; 1.在VS中鼠标对Translation Files文件右击&#xff0c;选择“添加”--->“模块”. 在弹窗的窗口中选择“Qt”--->“Qt Translation File”。 添加Translation_e…

BIOS MBR UEFI GPT详解

先来看下名词 启动方式&#xff1a; BIOS&#xff1a;Basic Input Output System&#xff0c;中文名称"基本输入输出系统"。 UEFI&#xff1a;Unified Extensible Firmware Interface&#xff0c;中文名称"统一的可扩展固件接口"。 Legacy&#xff1a;…

DAY38 动态规划 + 509. 斐波那契数 + 70. 爬楼梯 + 746. 使用最小花费爬楼梯

动态规划理论 动态规划&#xff0c;Dynamic Programming&#xff0c; DP&#xff0c; 如果某一问题有很多重叠子问题&#xff0c;使用动态规划是最有效的。 所以动态规划中每一个状态一定是由上一个状态推导出来的&#xff0c;这一点就区分于贪心&#xff0c;贪心没有状态推导…

【Git】HEAD detached from xxx 问题及解决方案

问题背景 最近用git的时候遇到了一个问题&#xff0c;场景是这样的。 我有一个分支main&#xff0c;其中有两个commit A和B&#xff0c;A是最新commit&#xff0c;B是历史commit。我先切到B看了看之前的代码&#xff0c;然后切到A&#xff0c;并进行了一些代码修改&#xff0…

AD教程(一)工程组成及创建

AD教程&#xff08;一&#xff09;工程组成及创建 工程组成 原理图库 绘制电阻模型、芯片模型、电容模型等&#xff0c;即将元件模型绘制出来。 原理图 将绘制的原件模型放置到原理图中&#xff0c;然后再添加连接的导线、网络标号。器件和器件之间的连接关系&#xff0c;在原…

SDL事件处理以及线程使用(2)

事件使用 #include <stdio.h> #include <SDL.h>#define FF_QUIT_EVENT (SDL_USEREVENT 1) // 定义自定义事件#undef main int main() {SDL_Window* pWindow NULL;SDL_Init(SDL_INIT_VIDEO);// 创建窗口pWindow SDL_CreateWindow("Event Test Title&…

【方法】如何给PDF文件添加“打开密码”?

PDF文件可以在线浏览&#xff0c;但如果想要给文件添加“打开密码”&#xff0c;就需要用到软件工具&#xff0c;下面小编分享两种常用的工具&#xff0c;小伙伴们可以根据需要选择。 工具一&#xff1a;PDF编辑器 PDF阅读器一般是没有设置密码的功能模块&#xff0c;PDF编辑器…

深入浅出排序算法之希尔排序

目录 1. 原理 2. 代码实现 3. 性能分析 1. 原理 希尔排序法又称缩小增量法。希尔排序法的基本思想是&#xff1a;先选定一个整数&#xff0c;把待排序文件中所有记录分成个组&#xff0c;所有距离为的记录分在同一组内&#xff0c;并对每一组内的记录进行排序。然后&#xf…

7.scala方法初探

概述 在 scala 中&#xff0c;方法定义在内中&#xff0c;这点类似于 java &#xff0c;此文说明如何定义方法&#xff0c;及方法一些 用法 相关链接 阅读之前&#xff0c;可以先行浏览一下 官方文档 scala相关文章 定义一个参数的方法 这个例子定义了一个名为 double 方法&a…

【Linux】权限完结

个人主页点击直达&#xff1a;小白不是程序媛 系列专栏&#xff1a;Linux被操作记 目录 前言 chown指令 chgrp指令 文件类型 file指令 目录的权限 粘滞位 umask指令 权限总结 前言 上篇文章我们说到对于一个文件所属者和所属组都是同一个人时&#xff0c;使用所属者身…

Linux(Centos7)操作记录

1、nginx -t #Nginx配置文件检查 上述截图代表检查没问题 上述截图检查配置文件配置错误&#xff0c;并提示错误文件位置 2、systemctl restart nginx #重启Nginx 重启Nginx失败 3、systemctl status nginx.service #查看Nginx服务状态 80端口被占导致服务启动失败 4、n…

DVWA-SQL Injection SQL注入

概念 SQL注入&#xff0c;是指将特殊构造的恶意SQL语句插入Web表单的输入或页面请求的查询字符串中&#xff0c;从而欺骗后端Web服务器以执行该恶意SQL语句。 成功的 SQL 注入漏洞可以从数据库中读取敏感数据、修改数据库数据&#xff08;插入/更新/删除&#xff09;、对数据…

Vue实现首页导航和左侧菜单,介绍mock.js并实现登录注册间的跳转,实现左侧栏折叠效果,优化Main.vue组件,使用mock.js生成随机响应数据

目录 1. mockjs 1.1 mockjs介绍 1.2 mockjs使用步骤 1.2.1 安装mockjs依赖 1.2.2 在项目中引入mockjs 1.2.3 创建目录和文件 1.2.4 为每个组件准备模拟数据 1.2.5 测试 1.2.6 前端调试 1.2.7 mockjs生成随机响应数据 1.2.8 根据不同响应&#xff0c;给出不同提示 2…

在 Elasticsearch 中丰富你的 Elasticsearch 文档

作者&#xff1a;David Pilato 对于 Elasticsearch&#xff0c;我们知道联接应该在 “索引时” 而不是查询时完成。 本博文是一系列三篇博文的开始&#xff0c;因为我们可以在 Elastic 生态系统中采取多种方法。 我们将介绍如何在 Elasticsearch 中做到这一点。 下一篇博文将介…

Python深度学习实战-基于class类搭建BP神经网络实现分类任务(附源码和实现效果)

实现功能 上篇文章介绍了用Squential搭建BP神经网络&#xff0c;Squential可以搭建出上层输出就是下层输入的顺序神经网络结构&#xff0c;无法搭出一些带有跳连的非顺序网络结构&#xff0c;这个时候我们可以选择类class搭建封装神经网络结构。 第一步&#xff1a;import ten…