Linux知识点 -- Linux多线程(四)
Linux知识点 – Linux多线程(四)
文章目录
- Linux知识点 -- Linux多线程(四)
- 一、线程池
- 1.概念
- 2.实现
- 3.单例模式的线程池
- 二、STL、智能指针和线程安全
- 1.STL的容器是否是线程安全的
- 2.智能指针是否是线程安全的
- 三、其他常见的各种锁
- 四、读者写者问题
- 1.读写锁
- 2.读写锁接口
一、线程池
1.概念
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
- 预先申请资源,用空间换时间;
- 预先申请一批线程,任务到来就处理;
- 线程池就是一个生产消费模型;
2.实现
thread.hpp
线程封装:
#pragma once#include<iostream>
#include<string>
#include<functional>
#include<cstdio>typedef void* (*fun_t)(void*); // 定义函数指针类型,后面回调class ThreadData // 线程信息结构体
{
public:void* _args;std::string _name;
};class Thread
{
public:Thread(int num, fun_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); // 直接将_tdata作为参数传给回调函数}void join() // 线程等待{pthread_join(_tid, nullptr);}std::string name(){return _name;}~Thread(){}private:std::string _name;fun_t _func;ThreadData _tdata;pthread_t _tid;
};
lockGuard.hpp
锁的封装,构建对象时直接加锁,对象析构时自动解锁;
#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;
};
log.hpp
#pragma once#include<iostream>
#include<cstdio>
#include<cstdarg>
#include<ctime>
#include<string>//日志级别
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4const char* gLevelMap[] = {"DEBUG","NORMAL","WARNING","ERROR","FATAL"
};#define LOGFILE "./threadpool.log"//完整的日志功能,至少需要:日志等级 时间 支持用户自定义(日志内容,文件行,文件名)void logMessage(int level, const char* format, ...)
{
#ifndef DEBUG_SHOWif(level == DEBUG) return;
#endifchar stdBuffer[1024];//标准部分time_t timestamp = time(nullptr);snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%ld] ", gLevelMap[level], timestamp);char logBuffer[1024];//自定义部分va_list args;va_start(args, format);vsnprintf(logBuffer, sizeof(logBuffer), format, args);va_end(args);FILE* fp = fopen(LOGFILE, "a");fprintf(fp, "%s %s\n", stdBuffer, logBuffer);fclose(fp);
}
- 注:
(1)提取可变参数

使用宏来提取可变参数:

将可变参数格式化打印到对应地点:

format是打印的格式;

(2)条件编译:

条件编译,不想调试的时候,就不加DEBUG宏,不打印日志信息;

-D:在命令行定义宏 ;
threadPool.hpp
线程池封装:
#include "thread.hpp"
#include <vector>
#include <queue>
#include <unistd.h>
#include "log.hpp"
#include "Task.hpp"
#include "lockGuard.hpp"const int g_thread_num = 3;template <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;}ThreadPool(int thread_num = g_thread_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));// 线程构造传入的this指针,是作为ThreadData结构体的参数的,ThreadData结构体才是routine回调函数的参数}}void run(){for (auto &iter : _threads){iter->start();logMessage(NORMAL, "%s %s", iter->name().c_str(), "启动成功");}}// 消费过程:线程调用回调函数取任务就是所谓的消费过程,访问了临界资源,需要加锁static void *routine(void *args){ThreadData *td = (ThreadData *)args;ThreadPool<T> *tp = (ThreadPool<T> *)td->_args; // 拿到this指针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; // 条件变量
};
- 注:
(1)如果回调函数routine放在thread类里面,由于成员函数会默认传this指针,因此参数识别的时候可能会出错,所以需要设置成静态成员;

(2)如果设置成静态类内方法,这个函数只能使用静态成员,而不能使用其他类内成员;
可以让routine函数拿到整体对象,在构造线程的时候,routine的参数传入this指针;

在构造函数的初始化列表中是参数的初始化,在下面的函数体中是赋值的过程,因此在函数体中对象已经存在了,就可以使用this指针了;
(3)类内公有接口让静态成员函数routine通过this指针能够访问类内成员;

testMain.cc
#include"threadPool.hpp"
#include"Task.hpp"
#include<ctime>
#include<cstdlib>
#include<iostream>
#include<unistd.h>int main()
{srand((unsigned long)time(nullptr) ^ getpid());ThreadPool<Task>* tp = new ThreadPool<Task>();tp->run();while(true){//生产的时候,只做任务要花时间int x = rand()%100 + 1;usleep(7756);int y = rand()%30 + 1;Task t(x, y, [](int x, int y)->int{return x + y;});logMessage(DEBUG, "制作任务完成:%d+%d=?", x, y);//推送任务到线程池中tp->pushTask(t);sleep(1);}return 0;
}
运行结果:

3.单例模式的线程池
threadPool.hpp
#include "thread.hpp"
#include <vector>
#include <queue>
#include <unistd.h>
#include "log.hpp"
#include "Task.hpp"
#include "lockGuard.hpp"const int g_thread_num = 3;template <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 = g_thread_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));// 线程构造传入的this指针,是作为ThreadData结构体的参数的,ThreadData结构体才是routine回调函数的参数}}ThreadPool(const ThreadPool<T> &other) = delete;const ThreadPool<T>& operator=(const ThreadPool<T> &other) = delete;public://创建单例对象的类内静态成员函数static ThreadPool<T>* getThreadPool(int num = g_thread_num){//在这里再加上一个条件判断,可以有效减少未来必定要进行的加锁检测的问题//拦截大量的在已经创建好单例的时候,剩余线程请求单例而直接申请锁的行为if(nullptr == _thread_ptr){//加锁lockGuard lockguard(&_mutex);//未来任何一个线程想要获取单例,都必须调用getThreadPool接口//一定会存在大量的申请锁和释放锁的行为,无用且浪费资源if(nullptr == _thread_ptr){_thread_ptr = new ThreadPool<T>(num);}}return _thread_ptr;}void run(){for (auto &iter : _threads){iter->start();logMessage(NORMAL, "%s %s", iter->name().c_str(), "启动成功");}}// 消费过程:线程调用回调函数取任务就是所谓的消费过程,访问了临界资源,需要加锁static void *routine(void *args){ThreadData *td = (ThreadData *)args;ThreadPool<T> *tp = (ThreadPool<T> *)td->_args; // 拿到this指针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; // 任务队列static ThreadPool<T>* _thread_ptr;static pthread_mutex_t _mutex;pthread_mutex_t _lock; // 锁pthread_cond_t _cond; // 条件变量
};//静态成员在类外初始化
template<class T>
ThreadPool<T>* ThreadPool<T>::_thread_ptr = nullptr;template<class T>
pthread_mutex_t ThreadPool<T>::_mutex = PTHREAD_MUTEX_INITIALIZER;


多线程同时调用单例过程,由于创建过程是非原子的,有可能被创建多个对象,是非线程安全的;
需要对创建对象的过程加锁,就可以保证在多线程场景当中获取单例对象;
但是未来任何一个线程想调用单例对象,都必须调用这个成员函数,就会存在大量申请和释放锁的行为;
可以在之间加一个对单例对象指针的判断,若不为空,就不进行对象创建;

testMain.cc
#include"threadPool.hpp"
#include"Task.hpp"
#include<ctime>
#include<cstdlib>
#include<iostream>
#include<unistd.h>int main()
{srand((unsigned long)time(nullptr) ^ getpid());//ThreadPool<Task>* tp = new ThreadPool<Task>();//tp->run(); ThreadPool<Task>::getThreadPool()->run();//创建单例对象while(true){//生产的时候,只做任务要花时间int x = rand()%100 + 1;usleep(7756);int y = rand()%30 + 1;Task t(x, y, [](int x, int y)->int{return x + y;});logMessage(DEBUG, "制作任务完成:%d+%d=?", x, y);//推送任务到线程池中ThreadPool<Task>::getThreadPool()->pushTask(t);sleep(1);}return 0;
}
运行结果:

二、STL、智能指针和线程安全
1.STL的容器是否是线程安全的
不是;
原因是, STL的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响;
而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶)。
因此STL默认不是线程安全。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全。
2.智能指针是否是线程安全的
对于unique_ ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题;
对于shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题.但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证shared_ptr 能够高效,原子的操作弓|用计数;
三、其他常见的各种锁
- 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等) ,当其他线程想要访问数据时,被阻塞挂起;
- 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作;
CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试; - 自旋锁
临界资源就绪的时间决定了线程等待的策略;
不断检测资源是否就绪就是自旋(轮询检测);
自旋锁本质就是通过不断检测锁状态,来检测资源是否就绪的方案;

互斥锁是检测到资源未就绪,就挂起线程;
临界资源就绪的时间决定了使用哪种锁;
四、读者写者问题
1.读写锁
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少,相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门]处理这种多读少写的情况呢?有,那就是读写锁。
-
读者写者模型与生产消费模型的本质区别:
生产消费模型中消费者会取走数据,而读者写者模型中读者不会取走数据; -
读锁的优先级高;
2.读写锁接口
-
初始化:

-
读者加锁:

-
写者加锁:

生产消费模型中,生产者和消费者的地位是对等的,这样才能达到最高效的状态
而读写者模型中,写者只有在读者全部退出的时候才能写,是读者优先的,这样就会发生写者饥饿问题;
读者写者问题中读锁的优先级高,是因为这种模型的应用场景为:数据的读取频率非常高,而被修改的频率特别低,这样有助于提升效率;
相关文章:
Linux知识点 -- Linux多线程(四)
Linux知识点 – Linux多线程(四) 文章目录 Linux知识点 -- Linux多线程(四)一、线程池1.概念2.实现3.单例模式的线程池 二、STL、智能指针和线程安全1.STL的容器是否是线程安全的2.智能指针是否是线程安全的 三、其他常见的各种锁…...
Java设计模式:四、行为型模式-07:状态模式
文章目录 一、定义:状态模式二、模拟场景:状态模式2.1 状态模式2.2 引入依赖2.3 工程结构2.4 模拟审核状态流转2.4.1 活动状态枚举2.4.2 活动信息类2.4.3 活动服务接口2.4.4 返回结果类 三、违背方案:状态模式3.0 引入依赖3.1 工程结构3.2 活…...
很多应用都是nginx+apache+tomcat
nginx 负责负载均衡,将大量的访问量平衡分配给多个服务器 apache 是用来处理静态html、图片等资源,在对HTML解析、响应等方面比tomcat效率更高。 tomcat 处理JSP等内容,进行后台业务操作。 upstream bbb.com.cn{ server 192.168.10.1:80 ;…...
原型模式:复制对象的技巧
欢迎来到设计模式系列的第六篇文章!在前面的几篇文章中,我们已经学习了一些常见的设计模式,今天我们将继续探讨另一个重要的设计模式——原型模式。 原型模式简介 原型模式是一种创建型设计模式,它主要用于复制对象。原型模式通…...
ClickHouse进阶(五):副本与分片-1-副本与分片
进入正文前,感谢宝子们订阅专题、点赞、评论、收藏!关注IT贫道,获取高质量博客内容! 🏡个人主页:含各种IT体系技术,IT贫道_Apache Doris,大数据OLAP体系技术栈,Kerberos安全认证-CSDN博客 📌订阅…...
Android 华为手机荣耀8X调用系统裁剪工具不能裁剪方形图片,裁剪后程序就奔溃,裁剪后获取不到bitmap的问题
买了个华为荣耀8X,安装自己写的App后,调用系统裁剪工具发现裁剪是圆形的,解决办法: //专门针对华为手机解决华为手机裁剪图片是圆形图片的问题 if (Build.MANUFACTURER.equals("HUAWEI")) {intent.putExtra("aspectX", 9998);intent.putExtra("a…...
《Flink学习笔记》——第十二章 Flink CEP
12.1 基本概念 12.1.1 CEP是什么 1.什么是CEP? 答:所谓 CEP,其实就是“复杂事件处理(Complex Event Processing)”的缩写;而 Flink CEP,就是 Flink 实现的一个用于复杂事件处理的库(…...
谷歌IndexedDB客户端存储数据
IndexedDB 具有以下主要特点: 1.存储大量数据:IndexedDB 可以存储大量的数据,比如存储离线应用程序的本地缓存或存储在线应用程序的大量数据。 2.结构化数据:IndexedDB 使用对象存储空间(Object Stores)来…...
天气数据的宝库:解锁天气预报API的无限可能性
前言 天气预报一直是我们日常生活中的重要组成部分。我们依赖天气预报来决定穿什么衣服、何时出行、规划户外活动以及做出关于农业、交通和能源管理等方面的重要决策。然而,要提供准确的天气预报,需要庞大的数据集和复杂的计算模型。这就是天气预报API的…...
插入排序(Insertion Sort)
C自学精简教程 目录(必读) 插入排序 每次选择未排序子数组中的第一个元素,从后往前,插入放到已排序子数组中,保持子数组有序。 打扑克牌,起牌。 输入数据 42 20 17 13 28 14 23 15 执行过程 完整代码 #include <iostream…...
2023蓝帽杯初赛
最近打完蓝帽杯 现在进行复盘 re 签到题 直接查看源代码 输出的内容就是 变量s 变量 number 而这都是已经设定好了的 所以flag就出来了 WhatisYourStory34982733 取证 案件介绍 取证案情介绍: 2021年5月,公安机关侦破了一起投资理财诈骗类案件&a…...
风险评估
风险评估概念 风险评估是一种系统性的方法,用于识别、评估和量化潜在的风险和威胁,以便组织或个人能够采取适当的措施来管理和减轻这些风险。 风险评估的目的 风险评估要素关系 技术评估和管理评估 风险评估分析原理 风险评估服务 风险评估实施流程...
直播软件app开发中的AI应用及前景展望
在当今数字化时代,直播市场蓬勃发展,而直播软件App成为人们获取实时信息和娱乐的重要渠道。随着人工智能(AI)技术的迅猛发展,直播软件App开发正逐渐融入AI的应用,为用户带来更智能、更个性化的直播体验。 …...
vscode html使用less和快速获取标签less结构
扩展插件里面搜索 css tree 插件 下载 使用方法 选择你要生成的标签结构然后按CTRLshiftp 第一次需要在输入框输入 get 然后选择 Generate CSS tree less结构就出现在这个里面直接复制到自己的less文件里面就可以使用了 在html里面使用less 下载 Easy LESS 插件 自己创建…...
excel中的引用与查找函数篇1
1、COLUMN(reference):返回与列号对应的数字 2、ROW(reference):返回与行号对应的数字 参数reference表示引用/参考单元格,输入后引用单元格后colimn()和row()会返回这个单元格对应的列号和行号。若参数reference没有引用单元格,…...
【python】—— 函数详解
前言: 本期,我们将要讲解的是有关python中函数的相关知识!!! 目录 (一)函数是什么 (二)语法格式 (三)函数参数 (四)函…...
springboot web开发登录拦截器
在SpringBoot中我们可以使用HandlerInterceptorAdapter这个适配器来实现自己的拦截器。这样就可以拦截所有的请求并做相应的处理。 应用场景 日志记录,可以记录请求信息的日志,以便进行信息监控、信息统计等。权限检查:如登陆检测ÿ…...
大数据课程K17——Spark的协同过滤法
文章作者邮箱:yugongshiye@sina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 了解Spark的协同过滤概念; 一、协同过滤概念 1. 概念 协同过滤是一种借助众包智慧的途径。它利用大量已有的用户偏好来估计用户对其未接触过的物品的喜好程度。其内在思想是相似度的定义…...
【力扣】1588. 所有奇数长度子数组的和 <前缀和>
【力扣】1588. 所有奇数长度子数组的和 给你一个正整数数组 arr ,请你计算所有可能的奇数长度子数组的和。子数组 定义为原数组中的一个连续子序列。请你返回 arr 中 所有奇数长度子数组的和 。 示例 1: 输入:arr [1,4,2,5,3] 输出&#x…...
socket,tcp,http三者之间的原理和区别
目录 1、TCP/IP连接 2、HTTP连接 3、SOCKET原理 4、SOCKET连接与TCP/IP连接 5、Socket连接与HTTP连接 socket,tcp,http三者之间的区别和原理 http、TCP/IP协议与socket之间的区别 下面的图表试图显示不同的TCP/IP和其他的协议在最初OSI模型中的位置…...
conda相比python好处
Conda 作为 Python 的环境和包管理工具,相比原生 Python 生态(如 pip 虚拟环境)有许多独特优势,尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处: 一、一站式环境管理:…...
超短脉冲激光自聚焦效应
前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应,这是一种非线性光学现象,主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场,对材料产生非线性响应,可能…...
golang循环变量捕获问题
在 Go 语言中,当在循环中启动协程(goroutine)时,如果在协程闭包中直接引用循环变量,可能会遇到一个常见的陷阱 - 循环变量捕获问题。让我详细解释一下: 问题背景 看这个代码片段: fo…...
Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级
在互联网的快速发展中,高性能、高效率和高安全性的网络服务成为了各大互联网基础设施提供商的核心追求。Cloudflare 作为全球领先的互联网安全和基础设施公司,近期做出了一个重大技术决策:弃用长期使用的 Nginx,转而采用其内部开发…...
什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南
文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果 SSE模式(远程调用) 4. 注册工具提…...
【JVM面试篇】高频八股汇总——类加载和类加载器
目录 1. 讲一下类加载过程? 2. Java创建对象的过程? 3. 对象的生命周期? 4. 类加载器有哪些? 5. 双亲委派模型的作用(好处)? 6. 讲一下类的加载和双亲委派原则? 7. 双亲委派模…...
stm32wle5 lpuart DMA数据不接收
配置波特率9600时,需要使用外部低速晶振...
从物理机到云原生:全面解析计算虚拟化技术的演进与应用
前言:我的虚拟化技术探索之旅 我最早接触"虚拟机"的概念是从Java开始的——JVM(Java Virtual Machine)让"一次编写,到处运行"成为可能。这个软件层面的虚拟化让我着迷,但直到后来接触VMware和Doc…...
