当前位置: 首页 > news >正文

【Linux】线程同步与互斥

文章目录

  • 1. 线程互斥
    • 1.1 进程线程间的互斥相关背景概念
    • 1.2 互斥量mutex
    • 1.3 相关操作
    • 1.4 互斥量实现原理
    • 1.5 互斥量的封装
  • 2. 线程同步
    • 2.1 条件变量
    • 2.2 生产者消费者模型
    • 2.3 基于BlockingQueue的生产者消费者模型
    • 2.4 信号量
    • 2.5 基于环形队列的生产消费模型
  • 3. 线程池
    • 3.1 日志
    • 3.2 线程池设计
  • 4. 线程安全与重入问题

在这里插入图片描述

1. 线程互斥

1.1 进程线程间的互斥相关背景概念

  • 临界资源:多线程执⾏流共享的资源就叫做临界资源
  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
  • 互斥:任何时刻,互斥保证有且只有⼀个执⾏流进⼊临界区,访问临界资源,通常对临界资源起保护作⽤
  • 原子性(后⾯讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成, 要么未完成。

1.2 互斥量mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。

多个线程并发的操作共享变量,会带来⼀些问题:
在这里插入图片描述
在上方代码中,由于每个线程在运行时可能被切换(特别是正在访问共享资源时),并且每个线程访问共享资源的代码不是原子的,所以会导致对资源的“重复”操作。

- - 操作并不是原⼦操作,而是对应三条汇编指令:

  • load :将共享变量g_ticket从内存加载到寄存器中
  • update : 更新寄存器里面的值,执行-1操作
  • store :将新值,从寄存器写回共享变量g_ticket的内存地址

要解决以上问题,需要做到三点:

  • 代码必须要有互斥行为:当代码进⼊临界区执行时,不允许其他线程进⼊该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许⼀个线程进⼊该临界区。
  • 如果当前线程不在临界区中执行,那么该线程不能阻止其他线程进⼊临界区

要做到这三点,本质上就是需要⼀把锁,Linux上提供的这把锁叫互斥量
在这里插入图片描述

1.3 相关操作

在Linux中,互斥量是一个类型为pthread_mutex_t类型的变量。

  • 锁的初始化与销毁

锁的初始化有两种方式:

  1. 静态分配

pthread_mutex_t mutex= PTHREAD_MUTEX_INITIALIZER

  1. 动态分配

在这里插入图片描述

锁的销毁:

在这里插入图片描述

销毁互斥量需要注意:

  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁⼀个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
  • 加锁与解锁的方法

在这里插入图片描述

lock是阻塞式的加锁;trylock若加锁失败,则会返回一个错误,下次什么时候再加锁,取决于程序员。

加了锁以后,对共享资源的访问就是安全的了。

在这里插入图片描述

调⽤ pthread_ lock 时,可能会遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  • 发起函数调⽤时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调⽤会陷⼊阻塞(执⾏流被挂起),等待互斥量解锁

1.4 互斥量实现原理

上面的代码中,锁是全局的,它保护临界资源,可是谁来保护锁呢?

所以pthread_mutex_loackpthread_mutex_unloack必须是原子的!

  • 经过上⾯的例⼦,⼤家已经意识到单纯的 i-- 或者 i++ 都不是原⼦的,有可能会有数据⼀致性问题
  • 为了实现互斥锁操作,⼤多数体系结构都提供了swap或exchange指令,该指令的作⽤是把寄存器和内存单元的数据相交换,由于只有⼀条指令,保证了原⼦性,即使是多处理器平台,访问内存的总线周期也有先后,⼀个处理器上的交换指令执⾏时另⼀个处理器的交换指令只能等待总线周期。

下面是lock和unlock的伪代码分析:

在这里插入图片描述

1.5 互斥量的封装

#include <iostream>
using namespace std;
#include <pthread.h>namespace MyMutexModule
{class Mutex{public:Mutex(const Mutex &) = delete;const Mutex &operator=(const Mutex &) = delete;Mutex(){pthread_mutex_init(&_mutex, nullptr);}~Mutex(){pthread_mutex_destroy(&_mutex);}void lock(){int n = pthread_mutex_lock(&_mutex);if (n != 0){cout << "lock fail" << endl;exit(1);}}void unlock(){int n = pthread_mutex_unlock(&_mutex);if (n != 0){cout << "unlock fail" << endl;exit(1);}}pthread_mutex_t *getAddr(){return &_mutex;}private:pthread_mutex_t _mutex;};//采用RAII风格,进行锁的管理class MutexGuard{public:MutexGuard(Mutex &mutex): _mutex(mutex){_mutex.lock();}~MutexGuard(){_mutex.unlock();}private:Mutex &_mutex;};
}

2. 线程同步

在这里插入图片描述

在上方抢票的案例中,我们发现总是会出现同一个线程一直在抢票的情况。这种情况可以但是不合乎情理,为了解决该情况,引入了线程同步的概念。

线程同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题

2.1 条件变量

条件变量就是用来实现线程同步效果的,内部要维护一个等待队列

当一个线程到来时,它发现没有资源可访问,那它就要去条件变量下等,直至条件满足,它被唤醒。

  1. 条件变量是一个pthread_cond_t类型的
  2. 初始化与销毁

在这里插入图片描述

  1. 等待

在这里插入图片描述
为什么 pthread_cond_wait 需要互斥量?- - 后面说

  1. 唤醒

在这里插入图片描述

测试:

在这里插入图片描述
在这里插入图片描述
此时,利用条件变量,就可以实现线程同步的问题了。

封装:


#pragma once
#include<pthread.h>
using namespace std;
#include"MyMute.hpp"
using namespace MutexModule;namespace MyCondModule
{class Cond{public:Cond(){pthread_cond_init(&_cond,nullptr);}//要让线程释放曾经拥有的锁!!void wait(Mutex& mutex){int n = pthread_cond_wait(&_cond,mutex.getAddr());(void)n;}void signal(){int n = pthread_cond_signal(&_cond);(void)n;}void signalAll(){int n = pthread_cond_broadcast(&_cond);(void)n;}~Cond(){pthread_cond_destroy(&_cond);}private:pthread_cond_t _cond;};
}

2.2 生产者消费者模型

生产者消费者模式:就是通过⼀个容器来解决生产者和消费者的强耦合问题,处理多个生产者线程和多个消费者线程之间的协作问题。

生产者和消费者彼此之间不直接通讯,而是通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

这个阻塞队列就是用来给生产者和消费者解耦的。

在这里插入图片描述

利用 “321”原则 牢记生产者消费者模式

  • 生产者消费者模式是多线程并发的一种模式,它有3 种关系,即生产者与生产者(互斥)、消费者与消费者(互斥)、消费者与生产者(互斥且同步
  • 2 种角色,生产则与消费者,通常由线程承担
    1 种特定的数据结构提供的缓冲区,充当交易场所

2.3 基于BlockingQueue的生产者消费者模型

在多线程编程中阻塞队列(Blocking Queue)是⼀种常用于实现生产者和消费者模型的数据结构。

其与普通的队列区别在于:

  • 当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放⼊了元素;
  • 当队列为满时,往队列⾥存放元素的操作也会被阻塞,直到有元素被从队列中取出

实现:

#pragma once
#include <iostream>
#include <pthread.h>
#include <queue>
using namespace std;namespace BlockQueueModule
{static const int gcap = 10;template <class T>class BlockQueue{private:bool isFull(){return _q.size() == _cap;}bool isEmpty(){return _q.empty();}public:BlockQueue(int cap = 10): _cap(cap),_cwait_num(0),_pwait_num(0){pthread_mutex_init(&_mutex,nullptr);pthread_cond_init(&consumer_cond,nullptr);pthread_cond_init(&productor_cond,nullptr);}void Push(const T &data){pthread_mutex_lock(&_mutex);while(isFull()) //为避免造成伪唤醒问题,应使用while循环判断{_pwait_num++;cout << "生产者进入等待..." << endl;//等待时,持有锁去等,会造成死锁问题;//因此wait内部会先释放锁,被唤醒后需要再次申请锁pthread_cond_wait(&productor_cond,&_mutex);cout << "生产者被唤醒..." << endl;_pwait_num--;}//不满 || 被唤醒,一定能生产!_q.push(data);//有资源能消费,唤醒消费者★★★★if(_cwait_num){cout << "唤醒消费者..." << endl;pthread_cond_signal(&consumer_cond);  //多生产多消费时,若在解锁之后唤醒被唤醒的线程未竞争过其它线程,//其它线程将资源使用完了,当前线程才被唤醒,然后去错误的使用资源,会造成伪唤醒问题。}pthread_mutex_unlock(&_mutex);}void Pop(T *out){pthread_mutex_lock(&_mutex);//若空,消费者应去其条件变量下去等while(isEmpty()) //为避免造成伪唤醒问题,应使用while循环判断{_cwait_num++;cout << "消费者进入等待..." << endl;pthread_cond_wait(&consumer_cond,&_mutex);cout << "消费者被唤醒..." << endl;_cwait_num--;}//不空 || 被唤醒,一定能消费!*out = _q.front();_q.pop();//没有有资源能消费,唤醒生产者if(_pwait_num){cout << "唤醒生产者..." << endl;pthread_cond_signal(&productor_cond);}pthread_mutex_unlock(&_mutex);}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&consumer_cond);pthread_cond_destroy(&productor_cond);}private:queue<T> _q; // 充当交易场所int _cap;    // 阻塞队列的最大容量pthread_mutex_t _mutex;        // 互斥pthread_cond_t consumer_cond;  // 消费者的条件变量pthread_cond_t productor_cond; // 生产者的条件变量int _cwait_num;int _pwait_num;};
}

在这里插入图片描述


为什么 pthread_cond_wait 需要传递互斥量呢??

  • 条件等待是线程间同步的⼀种⼿段,如果只有⼀个线程,条件不满⾜,⼀直等下去都不会满⾜,所以必须要有⼀个线程通过某些操作,改变共享变量,使原先不满⾜的条件变得满⾜,并且友好的通知等待在条件变量上的线程。

在这里插入图片描述

  • 在上面的代码中,由于生产者与消费者是互斥的,那他们俩用的就是同一把锁;如果某一方 pthread_cond_wait时,是持有锁去等的话,另一方就永远无法获得锁,也就无法改变共享资源,从而无法改变条件变量,就会造成死锁得局面。
  • 所以,在一方进行等待时,需要先将持有的锁释放,让另一方能够有机会去改变条件变量,从而唤醒等待的一方;等待的一方被唤醒后,需再次持有锁对共享资源进行操作。

在我们上面所实现的生产者消费者模型中,生产者与消费者是互斥的,也就是说在任何一个时刻,只能有一方访问共享资源,那不就是串行执行了吗?它高效在哪里呢?

生产者向交易场所中生产数据,消费者从交易场所中获取数据确实是串行的。
可是生产者的数据从哪里来呢?消费者获取的数据又如何处理呢?
在这里插入图片描述

2.4 信号量

信号量的使用主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。

POSIX信号量和SystemV信号量作⽤相同,但POSIX可以⽤于线程间同步。

在我们前面所写的代码中,都是对资源进行整体使用的,生产者用消费者就不能用;可以使用信号量对资源进行拆分,使得生产者与消费者某些时候可以同时使用共享资源

相关接口:

  • 初始化

在这里插入图片描述

参数:

  • pshared:0表示线程间共享非零表示进程间共享
  • value:信号量初始值(信号量的个数)
  • 销毁

在这里插入图片描述
在这里插入图片描述

  • 发布信号量

功能:发布信号量,表⽰资源使⽤完毕,可以归还资源了。将信号量值加1。

在这里插入图片描述

  • 等待信号量

功能:等待信号量,会将信号量的值减1

在这里插入图片描述

封装:

#pragma once
#include <semaphore.h>
using namespace std;namespace MySemModule
{const int g_value = 10;class Sem{public:Sem(int value = g_value):_value_num(value){sem_init(&_sem,0,_value_num);}void P(){int n = sem_wait(&_sem);(void)n;}void V(){int n = sem_post(&_sem);(void)n;}~Sem(){sem_destroy(&_sem);}private:sem_t _sem;int _value_num;};
} // namespace MySemModule

2.5 基于环形队列的生产消费模型

)

#pragma once#include <semaphore.h>
#include <iostream>
#include <pthread.h>
using namespace std;
#include <vector>#include "MyMute.hpp"
#include "MySem.hpp"namespace RingBufferModule
{using namespace MyMutexModule;using namespace MySemModule;const int g_default_cap = 10;template <class T>class RingBuffer{public:RingBuffer(int cap = g_default_cap): _ring(cap), _cap(cap), _c_head(0), _p_tail(0), _space_sem(cap)   // 初始化空间信号量, _data_sem(0)      // 初始化数据信号量{// sem_init(&_data_sem, 0, 0);     // 初始化数据信号量// sem_init(&_space_sem, 0, _cap); // 初始化空间信号量}void Push(T &data){// 1. 先获取信号量  -->有,获取成功;没有,阻塞// sem_wait(&_space_sem);_space_sem.P();// 多生产者时,不上锁,会造成空间数据的丢失(覆盖),可能会使用同一个_p_tail{LockGuard lockguard(_space_lock);_ring[_p_tail] = data;_p_tail++;_p_tail %= _cap;}// sem_post(&_data_sem); // 数据信号量增加_data_sem.V();}void Pop(T *out){// sem_wait(&_data_sem);_data_sem.P();// 多消费者时,不上锁,会造成空间数据的重复消费,可能会使用同一个_c_head{LockGuard lockguard(_data_lock);*out = _ring[_c_head];_c_head++;_c_head %= _cap;}_space_sem.V();// sem_post(&_space_sem); // 空间信号量增加}~RingBuffer(){// sem_destroy(&_data_sem);// sem_destroy(&_space_sem);}private:vector<T> _ring; // 环,临界资源int _cap;        // 总容量int _c_head;int _p_tail;Sem _data_sem;  // 数据信号量Sem _space_sem; // 空间信号量Mutex _space_lock; // 空间锁Mutex _data_lock;  // 数据锁};} // namespace RingBufferModule

3. 线程池

下⾯开始,我们结合我们之前所做的所有封装,进⾏⼀个线程池的设计。在写之前,我们要做如下准备

  • 准备线程的封装 (已有)
  • 准备锁和条件变量的封装 (已有)
  • 引入日志,对线程进行封装

3.1 日志

计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题并⽀持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要⼯具。

日志格式以下几个指标是必须得有的:

  • 时间戳
  • 日志等级
  • 日志内容

以下几个指标是可选

  • 文件名
  • 行号
  • 进程,线程相关id信息等

日志有现成的解决放案,如:spdlog、glog、Boost.Log、Log4cxx等等,我们依旧采用自定义日志的方式。

这里我们采用设计模式,策略模式(基类规定方法,子类去重写)来进行日志的设计:

  • 控制台日志策略
  • 文件日志策略
	// 基类class LogStrategy{public:virtual ~LogStrategy() = default;virtual void FlushLog(const std::string &message) = 0; // 派生类必须重写};// 控制台 派生类class ConsoleStrategy : public LogStrategy{public:virtual ~ConsoleStrategy() override{// std::cout << "~ConsoleStrategy" << std::endl;}virtual void FlushLog(const std::string &message) override // 派生类重写基类的虚函数{LockGuard LockGuard(_mutex);std::cerr << message << std::endl;}private:Mutex _mutex; // 显示器也是临界资源,保证输出线程的安全};// 文件 派生类class FileStrategy : public LogStrategy{public:FileStrategy(const std::string logPaht = defaultLogPath, const std::string logFileName = defaulFiletName): _logPath(defaultLogPath), _logFileName(defaulFiletName){LockGuard LockGuard(_mutex);// filesystem c++17if (std::filesystem::exists(_logPath)) // 检测当前路径是否存在return;try{std::filesystem::create_directories(_logPath);}catch (const std::filesystem::filesystem_error &e){std::cerr << e.what() << std::endl;}}virtual ~FileStrategy() override{std::cout << "~FileStrategy" << std::endl;}virtual void FlushLog(const std::string &message) override // 派生类重写基类的虚函数{LockGuard lockguard(_mutex);std::string filename = _logPath + _logFileName;// 使用ofstream打开文件filename,以append方式std::ofstream out(filename.c_str(), std::ofstream::app);if (!out.is_open()){std::cerr << filename << " 文件打开失败" << std::endl;return; // 打开失败}out << message << "\n"; // 向文件中写}private:std::string _logPath;std::string _logFileName;Mutex _mutex; // 文件也是临界资源,保证输出线程的安全};

具体的日志类:

注意点:

  • 内部类使用外部类,内部类中必须是外部类的引用。
  • 引用必须在初始化列表内初始化
	class Logger{private:std::unique_ptr<LogStrategy> _strategy; // 日志的写入策略public:Logger(){UseConsoleStrategy(); // 默认使用控制台策略}~Logger(){}void UseConsoleStrategy(){_strategy = std::make_unique<ConsoleStrategy>();}void UseFileStrategy(){_strategy = std::make_unique<FileStrategy>();}// 内部类//[可读性很好的时间] [⽇志等级] [进程pid] [打印对应⽇志的⽂件名][⾏号] - 消息内容(⽀持可变参数)class LogMessage{private:std::string _cur_time; // 日志写入时间LogGrade _grade;       // 日志等级pid_t _pid;            // 进程pidstd::string _fileName; // 文件名int _lineNum;          // 行号Logger &_logger;       // 方便进行日志的刷新,一定是引用std::string _completeMessage; // 完整的信息public:LogMessage(LogGrade grade, std::string filename, int lineNum, Logger &logger): _cur_time(GetCurTime()), _grade(grade), _pid(getpid()), _fileName(filename), _lineNum(lineNum), _logger(logger){// stringstream不允许拷⻉,所以这⾥就当做格式化功能使⽤std::stringstream ssbuffer;ssbuffer << "[" << _cur_time << "] "<< "[" << GradeToString(_grade) << "] "<< "[" << _pid << "] "<< "[" << _fileName << "] "<< "[" << _lineNum << "] "<< " - ";_completeMessage = ssbuffer.str(); // 信息格式以初始化完成}// LogMessage(const LogMessage& lg)//     :_logger(lg._logger)// {//     _cur_time = lg._cur_time;//     _grade = lg._grade;//     _pid = lg._pid;//     _fileName = lg._fileName;//     _lineNum = lg._lineNum;//     cout <<"拷贝构造:LogMessage(const LogMessage& lg)" << endl;// }// RAII⻛格,析构的时候进⾏⽇志持久化,采⽤指定的策略~LogMessage(){if (_logger._strategy){_logger._strategy->FlushLog(_completeMessage);cout <<"~LogMessage" << endl;}}// 为支持连续的 LogMessage << "11" << "222";template <class T>LogMessage &operator<<(const T &message){stringstream ssbuffer;ssbuffer << message;_completeMessage += ssbuffer.str(); // 每次将输入信息添加到日子信息后return *this;}}; // class LogMessage// 故意拷⻉,形成LogMessage临时对象,临时对象内包含独⽴⽇志数据,后续在被<<时,会被持续引⽤,// 直到完成输⼊,才会⾃动析构临时LogMessage,⾄此也完成了⽇志的显⽰或者刷新// 未来采⽤宏替换,进⾏⽂件名和代码⾏数的获取public:LogMessage operator()(LogGrade grade, const std::string &filename, int lineNum){return LogMessage(grade, filename, lineNum, *this);//传值返回,若没有对象接收,拷贝构造会被编译器优化掉!!!!!!!}}; // class Logger// 定义全局的Logger 对象Logger logger;// 使⽤宏,可以进⾏代码插⼊,⽅便随时获取⽂件名和⾏号
#define LOG(grade) logger(grade, __FILE__, __LINE__)// 提供选择使⽤何种⽇志策略的⽅法
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy()
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy()

在这里插入图片描述

3.2 线程池设计

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

线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价

  • 线程池的应用场景:

    • 需要大量的线程来完成任务,且完成任务的时间比较短。 比如WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象⼀个热门网站的点击次数。 但对于长时间的任务,比如⼀个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
    • 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  • 线程池的种类

    • 创建固定数量线程池,循环从任务队列中获取任务对象,获取到任务对象后,执⾏任务对象中的任务接口
    • 浮动线程池,其他同上

在这里插入图片描述
在实现线程池时,我们全部都使用自己封装过的接口。

#include <iostream>
#include <memory>
#include <queue>
#include <vector>#include "Log.hpp"
#include "MyMute.hpp"
#include "myCond.hpp"
#include "Thread.hpp"namespace ThreadPoolModule
{using namespace MyCondModule;using namespace MyLogModule;using namespace MyMutexModule;using namespace MyThreadModule;static const int default_count = 10;using thread_t = shared_ptr<MyThread>;void DefaultTest(){while (true){LOG(NORMAL) << "测试线程的执行方法";sleep(1);}}template <class T>class ThreadPool{private:queue<T> _task_q;          // 任务队列vector<thread_t> _threads; // 管理线程的结构int _threadCount;          // 线程的数量int _wait_num;Mutex _lock;Cond _cond;bool _isRunning;bool isEmpty(){return _task_q.empty();}void HandleTask(std::string name){LOG(NORMAL) << name << "进入HandleTask方法";// 线程要一直做该任务while (true){// 1. 拿任务T t;{LockGuard lockguard(_lock);//任务队列为空,线程池没退,则进程必须等while (isEmpty() && _isRunning){_wait_num++;_cond.wait(_lock);_wait_num--;}//任务队列为空,线程池退了,则进程自己退出if(isEmpty() && !_isRunning)break;//任务队列不为空,线程池退/没退,线程都执行任务完任务t = _task_q.front();_task_q.pop();}// 2. 处理任务t(name);}}public:ThreadPool(int count = default_count): _threadCount(count), _wait_num(0), _isRunning(false){// 构建线程对象for (int i = 0; i < _threadCount; i++){// 构造MyThread时,将HandleTask方法与其绑定,传递this指针是为了访问它。//_threads.push_back(make_shared<MyThread>(bind(&ThreadPool::HandleTask, this, std::placeholders::_1)));_threads.push_back(make_shared<MyThread>([this](std::string name){ this->HandleTask(name); }));LOG(NORMAL) << "构建线程对象" << _threads.back()->name() << "...成功";}}~ThreadPool(){}void Start(){if (_isRunning)return;_isRunning = true; // 线程池运行标记// 启动线程for (auto &thread_ptr : _threads){thread_ptr->create();LOG(NORMAL) << "启动线程" << thread_ptr->name() << "...成功";}}void Wait(){// 等待线程for (auto &thread_ptr : _threads){thread_ptr->join();LOG(NORMAL) << "等待线程" << thread_ptr->name() << "...成功";}}void Stop(){LockGuard lockguard(_lock);if (_isRunning){_isRunning = false;// 1.不能再次放任务了--标记位// 2.让线程自己结束if(_wait_num)_cond.signalAll(); //唤醒所有线程// 3.历史任务都要被执行完了}LOG(NORMAL) << "线程池stop..";}void Push(T in){LockGuard lockguard(_lock);if (!_isRunning)return;_task_q.push(in);if (_wait_num)_cond.signal();}};} // namespace ThreadPoolModule

在这里插入图片描述

4. 线程安全与重入问题

线程安全:就是多个线程在访问共享资源时,能够正确地执行,不会相互干扰或破坏彼此的执行结果。

⼀般而言,多个线程并发同⼀段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进⾏操作,并且没有锁保护的情况下,容易出现该问题。

函数是可重入的,那就是线程安全的!!!

线程安全函数:若线程执行的函数是线程安全的函数,该函数叫做线程安全函数。
可重入函数是线程安全函数的⼀种。

注意:

  • 线程安全不⼀定是可重入的,而可重⼊函数则⼀定是线程安全的。
    如果对临界资源的访问加上锁,则这个函数是线程安全的;但如果这个重入函数在锁还未释放之前再次进入,则会产⽣死锁,因此是不可重入的(例如递归、信号导致⼀个执行流重复进⼊函数)

我们上面所写的线程池还存在一些问题,例如线程池的个数不是想创建多少就创建多少的,这还取决于OS;为了避免线程安全问题,还需要对线程池的创建进行加锁。

因此我们可使用单例模式,设计一个只允许创建一个的线程池,并对其加锁。

在这里插入图片描述


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

不是。

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

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

  • 对于 unique_ptr, 由于只是在当前代码块范围内⽣效, 因此不涉及线程安全问题。
  • 对于 shared_ptr,多个对象需要共⽤⼀个引⽤计数变量, 所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题, 基于原⼦操作(CAS)的⽅式保证shared_ptr 能够⾼效,原⼦的操作引用计数
  • 注意:shared_ptr是线程安全的,但是使用其对某些资源进行操作不是,需要操作者自行保证。

在这里插入图片描述

相关文章:

【Linux】线程同步与互斥

文章目录 1. 线程互斥1.1 进程线程间的互斥相关背景概念1.2 互斥量mutex1.3 相关操作1.4 互斥量实现原理1.5 互斥量的封装 2. 线程同步2.1 条件变量2.2 生产者消费者模型2.3 基于BlockingQueue的生产者消费者模型2.4 信号量2.5 基于环形队列的生产消费模型 3. 线程池3.1 日志3.…...

设计模式-原型模式

背景 克隆羊&#xff1a;有一只羊&#xff0c;有各种属性&#xff1a;姓名&#xff0c;年龄……&#xff0c;现在要克隆10只和这只羊一模一样的羊。 传统方法&#xff1a; 定义一个羊类&#xff0c;在客户端调取原型羊的信息&#xff0c;根据信息创建10个属性相同的羊。 问…...

异或操作解决一些问题

前提&#xff1a; 异或操作符合交换律&#xff0c;结合律&#xff08;因为其根本上来抽象理解&#xff0c;就是查看所有项二进制数相同位是否有奇数个1&#xff0c;对运算结果二进制数而言&#xff0c;没有该位为0&#xff0c;有该位为1&#xff0c;与顺序无关&#xff09;。 …...

Rust中Tracing 应用指南

欢迎来到这篇全面的Rust跟踪入门指南。Rust 的tracing是一个用于应用程序级别的诊断和调试的库。它提供了一种结构化的、异步感知的方式来记录日志和跟踪事件。与传统的日志记录相比&#xff0c;tracing能够更好地处理复杂的异步系统和分布式系统中的事件跟踪&#xff0c;帮助开…...

Java与C#

Java和C#&#xff08;C Sharp&#xff09;是两种流行的面向对象编程语言&#xff0c;它们在很多方面非常相似&#xff0c;因为它们都受到了类似的编程范式和语言设计理念的影响。然而&#xff0c;它们之间也存在一些重要的区别。 平台依赖性&#xff1a; Java&#xff1a;Java是…...

Docker 部署 MongoDB

&#x1f680; 作者主页&#xff1a; 有来技术 &#x1f525; 开源项目&#xff1a; youlai-mall &#x1f343; vue3-element-admin &#x1f343; youlai-boot &#x1f343; vue-uniapp-template &#x1f33a; 仓库主页&#xff1a; GitCode&#x1f4ab; Gitee &#x1f…...

【C语言】字符串左旋的三种解题方法详细分析

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C语言 文章目录 &#x1f4af;前言&#x1f4af;题目描述&#x1f4af;方法一&#xff1a;逐字符移动法&#x1f4af;方法二&#xff1a;使用辅助空间法&#x1f4af;方法三&#xff1a;三次反转法&#x1f4af;方法对…...

Android导出Excel

poi org.apache.poi:poi-ooxml:4.x&#xff1a; 不支持Android使用&#xff0c; 不支持原因&#xff1a;Android底层库不支持xml所需的bean类&#xff0c;使用即报错only supported starting with Android O (–min-api 26) org.apache.poi:poi-ooxml:5.2.0&#xff1a; 支持A…...

【学术讲座】视觉计算中的深度学习方法 AIGC图像视频生成模型的推理加速

视觉计算中的深度学习方法 发展历程 backbone 强化学习、LLM等&#xff1a;有监督 && 无监督的结合 目标检测 图像分割 网络结构搜索 搜索方法 1&#xff1a;强化学习 2&#xff1a;强化学习 3&#xff1a;梯度算法 结构选择的作用 1&#xff1a;开放环境感知网络…...

华为OD机试真题---智能驾驶

华为OD机试中的“智能驾驶”题目是一道涉及广度优先搜索&#xff08;BFS&#xff09;算法运用的题目。以下是对该题目的详细解析&#xff1a; 一、题目描述 有一辆汽车需要从m * n的地图的左上角&#xff08;起点&#xff09;开往地图的右下角&#xff08;终点&#xff09;&a…...

视频质量评价SimpleVQA

目录 一、研究意义例子 二、介绍三、文章解读3.1 论文动机3.2论文思路3.3方法3.3.1网络框架3.3.2公式解读3.3.3核心创新3.3.4理解 &#xff01;&#xff01;&#xff01;作者对模型的改进本人算法框体视频抽帧美学特征提取网络&#xff1a;3.3.5实验细节&#xff1a; 四、代码复…...

浏览器插件基于nativeMessaging通信

上一篇文章介绍了基于nativeMessaging启动本地程序&#xff0c;使用官方demo是支持双向通信&#xff0c;demo启动的程序是python写的&#xff0c;现在基于Qt程序进行说明。 消息传递协议 Chrome 会在单独的进程中启动每个原生消息传递主机&#xff0c;并使用标准输入 (stdin)…...

解决 MySQL 8.x 身份验证问题的最佳实践20241126

MySQL 8.x 身份验证问题的深入解析与实践解决方案 &#x1f3af; 引言 &#x1f58b;️ MySQL 是全球最受欢迎的开源数据库之一&#xff0c;随着 MySQL 8.x 的发布&#xff0c;引入了更安全的身份验证插件 caching_sha2_password&#xff0c;显著提升了数据库的安全性和性能。…...

对于GC方面,在使用Elasticsearch时要注意什么?

大家好&#xff0c;我是锋哥。今天分享关于【对于GC方面&#xff0c;在使用Elasticsearch时要注意什么&#xff1f;】面试题。希望对大家有帮助&#xff1b; 对于GC方面&#xff0c;在使用Elasticsearch时要注意什么&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java…...

各种排序算法

前置知识 排序: 按照递增或者递减的顺序把数据排列好 稳定性: 值相等的元素在排序之后前后顺序是否发生了改变 内部排序: 数据放在内存上 外部排序: 数据放在磁盘上 内部排序 基于比较的排序 几大排序算法 1. 堆排序 特点: 思想: 1. 创建大根堆,把所有元素放在大根堆里…...

前端-Git

一.基本概念 Git版本控制系统时一个分布式系统&#xff0c;是用来保存工程源代码历史状态的命令行工具 简单来说Git的作用就是版本管理工具。 Git的应用场景&#xff1a;多人开发管理代码&#xff1b;异地开发&#xff0c;版本管理&#xff0c;版本回滚。 Git 的三个区域&a…...

用nextjs开发时遇到的问题

这几天已经基本把node后端的接口全部写完了&#xff0c;在前端开发时考虑时博客视频类型&#xff0c;考虑了ssr&#xff0c;于是选用了nextJs&#xff0c;用的是nextUi,tailwincss,目前碰到两个比较难受的事情。 1.nextUI个别组件无法在服务器段渲染 目前简单的解决方法&…...

Cannot find a valid baseurl for repo: centos-sclo-rh/x86_64

yum install 报错: Cannot find a valid baseurl for repo: centos-sclo-rh/x86_64 CentOS7的SCL源在2024年6月30日停止维护了。 当scl源里面默认使用了centos官方的地址&#xff0c;无法连接&#xff0c;需要替换为阿里云。 cd /etc/yum.repos.d/ 找到 CentOS-SCLo-scl.repo 和…...

HCIA笔记3--TCP-UDP-交换机工作原理

1. tcp协议 可靠的连接 1.1 报文格式 1.2 三次握手 1.3 四次挥手 为什么TIME_WAIT需要2MSL的等待时间&#xff1f; &#xff08;a&#xff09; 为了实现可靠的关闭 &#xff08;b&#xff09;为了让过期的报文在网络上消失 对于(a), 假设host发给server的last ack丢了。 ser…...

RabbitMQ原理架构解析:消息传递的核心机制

文章目录 一、RabbitMQ简介1.1、概述1.2、特性 二、RabbitMQ原理架构三、RabbitMQ应用场景3.1、简单模式3.2、工作模式3.3、发布订阅3.4、路由模式3.5 主题订阅模式 四、同类中间件对比五、RabbitMQ部署5.1、单机部署5.2、集群部署&#xff08;镜像模式&#xff09;5.3、K8s部署…...

C语言指针作业

//8-29 第八章作业 //3.输入10个整数,将其中最小的数第一个数对换,把最大的数与最后一个数对换 //写3个函数:①输人10个数;②进行处理;③输出10个数。 //①输人10个数&#xff0c; //方法一&#xff1a;数组索引 void main() {int arr[10];int *p arr;//printf("请输入…...

区块链:比特币-Binance

写在前面&#xff1a;相对于Tran和BNB而言&#xff0c;Binance不支持智能合约&#xff1b;大约每 10分钟 生成一个新区块 一、认识Binance 官方网站&#xff1a;Blockchain Explorer - Bitcoin Tracker & More | Blockchain.com 官方文档&#xff1a;Authentication – I…...

数据工程流程

** 数据工程流程图** #mermaid-svg-ArT55xCISSfZImy3 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-ArT55xCISSfZImy3 .error-icon{fill:#552222;}#mermaid-svg-ArT55xCISSfZImy3 .error-text{fill:#552222;stroke…...

使用Python和Pybind11调用C++程序(CMake编译)

目录 一、前言二、安装 pybind11三、编写C示例代码四、结合Pybind11和CMake编译C工程五、Python调用动态库六、参考 一、前言 跨语言调用能对不同计算机语言进行互补&#xff0c;本博客主要介绍如何实现Python调用C语言编写的函数。 实验环境&#xff1a; Linux gnuPython3.10…...

基于springboot的县市级土地使用监控系统的设计与实现

文末获取本系统&#xff08;程序源码数据库调试部署开发环境&#xff09;文末可获取&#xff0c;系统界面在最后面。 摘 要 如今社会上各行各业&#xff0c;都喜欢用自己行业的专属软件工作&#xff0c;互联网发展到这个时候&#xff0c;人们已经发现离不开了互联网。新技术的…...

Hot100 - 最大子数组和

Hot100 - 最大子数组和 最佳思路&#xff1a;动态规划 时间复杂度&#xff1a;O(n) 代码&#xff1a; class Solution {public int maxSubArray(int[] nums) {int sum 0;int ans Integer.MIN_VALUE;for (int i 0; i < nums.length; i) {// 如果 sum > 0&#xff0c…...

Matlab 深度学习 PINN测试与学习

PINN 与传统神经网络的区别 与传统神经网络的不同之处在于&#xff0c;PINN 能够以微分方程形式纳入有关问题的先验专业知识。这些附加信息使 PINN 能够在给定的测量数据之外作出更准确的预测。此外&#xff0c;额外的物理知识还能在存在含噪测量数据的情况下对预测解进行正则…...

[STM32]从零开始的STM32 FreeRTOS移植教程

一、前言 如果能看到这个教程的话&#xff0c;说明大家已经学习嵌入式有一段时间了。还记得嵌入式在大多数时候指的是什么吗&#xff1f;是的&#xff0c;我们所说的学习嵌入式大部分时候都是在学习嵌入式操作系统。从简单的一些任务状态机再到复杂一些的RTOS&#xff0c;再到最…...

软件团队的共担责任

问责制被认为是个人与其社会系统之间的纽带&#xff0c;它创造了一种将个人与其行为和绩效联系起来的身份关系。在入门系列的第一篇文章《超越工具和流程&#xff1a;成功软件开发团队的策略》中&#xff0c;我们介绍了问责制的概念&#xff0c;并提出了以下定义&#xff1a; …...

代码美学:MATLAB制作渐变色

输入颜色个数n&#xff0c;颜色类型&#xff1a; n 2; % 输入颜色个数 colors {[1, 0, 0], [0, 0, 1]}; createGradientHeatmap(n, colors); 调用函数&#xff1a; function createGradientHeatmap(n, colors)% 输入检查if length(colors) ~ nerror(输入的颜色数量与n不一…...

Perforce SAST专家详解:自动驾驶汽车的安全与技术挑战,Klocwork、Helix QAC等静态代码分析成必备合规性工具

自动驾驶汽车安全吗&#xff1f;现代汽车的软件包含1亿多行代码&#xff0c;支持许多不同的功能&#xff0c;如巡航控制、速度辅助和泊车摄像头。而且&#xff0c;这些嵌入式系统中的代码只会越来越复杂。 随着未来汽车的互联程度越来越高&#xff0c;这一趋势还将继续。汽车越…...

大模型中常见的微调方法有哪些?

我整理了1000道算法面试题&#xff1a; 获取 这里说的微调主要是指参数微调&#xff0c;参数微调的方法主要有以下几种&#xff1a; - Adapter 在预训练模型每一层(或某些层)中添加Adapter模块(如上图左侧结构所示)&#xff0c;微调时冻结预训练模型主体&#xff0c;由Ada…...

MATLAB支持的距离度量

距离度量是用于量化两个点或样本之间差异的一种方法。在不同的领域和应用场景中&#xff0c;距离度量的选择可能会有所不同。 欧几里得距离&#xff08;Euclidean Distance&#xff09;&#xff1a;这是最直观的距离定义&#xff0c;适用于n维空间中的两点。对于二维空间中的点…...

c++编程玩转物联网:使用芯片控制8个LED实现流水灯技术分享

在嵌入式系统中&#xff0c;有限的GPIO引脚往往限制了硬件扩展能力。74HC595N芯片是一种常用的移位寄存器&#xff0c;通过串行输入和并行输出扩展GPIO数量。本项目利用树莓派Pico开发板与74HC595N芯片&#xff0c;驱动8个LED实现流水灯效果。本文详细解析项目硬件连接、代码实…...

LSA详情与特殊区域

LSA是构成LSDB的重要原材料&#xff0c;在OSPF中发挥很大作用。 报文 通用头部 LS age&#xff1a;LSA寿命&#xff0c;0-3600s Options&#xff1a;可选项 LS type&#xff1a;LSA类型&#xff0c;三要素之一 Link State ID&#xff1a;LSAID 三要素之一 Advertising Ro…...

Leecode刷题C语言之交替组①

执行结果:通过 执行用时和内存消耗如下&#xff1a; 代码如下&#xff1a; int numberOfAlternatingGroups(int* colors, int colorsSize) {int res 0;for (size_t i 0; i < colorsSize; i) {if (colors[i] ! colors[(i - 1 colorsSize) % colorsSize] && col…...

深入解析 Django 中数据删除的最佳实践:以动态管理镜像版本为例

文章目录 引言场景与模型设计场景描述 删除操作详解1. 删除单个 Tag2. 批量删除 Tags3. 删除前确认4. 日志记录 高阶优化与问题分析1. 外键约束与误删保护2. 并发删除的冲突处理3. 使用软删除 结合 Django Admin 的实现总结与实践思考 引言 在现代应用开发中&#xff0c;服务和…...

4457数字示波器 2Gpts的深度存储

4457数字示波器 2Gpts的深度存储 256级灰度等级及四种波形色彩显示 4457M系列数字示波器&#xff0c;带宽从1GHz到4GHz&#xff0c;采样率10GSa/s、20GSa/s&#xff0c;垂直分辨率12bit&#xff0c;存储深度1Gpts&#xff0c;最快波形捕获率70万个波形/秒&#xff0c;独创的…...

【笔记】轻型民用无人驾驶航空器安全操控

《轻型民用无人驾驶航空器安全操控》 理论考试培训材料 法规部分 【民用无人驾驶航空器的分类】 1、如何定义微型、轻型无人驾驶航空器&#xff1f; 微型无人驾驶航空器&#xff0c;是指空机重量小于0.25千克&#xff0c;最大平飞速度不超过40千米/小时&#xff0c;无线电发…...

【leetcode】动态规划

31. 873. 最长的斐波那契子序列的长度 题目&#xff1a; 如果序列 X_1, X_2, ..., X_n 满足下列条件&#xff0c;就说它是 斐波那契式 的&#xff1a; n > 3对于所有 i 2 < n&#xff0c;都有 X_i X_{i1} X_{i2} 给定一个严格递增的正整数数组形成序列 arr &#xff0…...

嵌入式linux系统中图像处理基本方法

目录 2.1 BMP图像处理 2.1.1 BMP文件格式解析 2.1.2 代码实现:将BMP文件解析为RGB格式,在LCD上显示 2.2 JPEG图像处理 2.2.1 JPEG文件格式和libjpeg编译 2.2.2 libjpeg接口函数的解析和使用 2.2.3 使用libjpeg把JPEG文件解析为RGB格式,在LCD上显示 …...

Qt SQL模块概述

Qt SQL支持的数据库 要在项目中使用 Qt SQL 模块&#xff0c;需要在项目配置文件中添加下面一条设置语句&#xff1a; Qt sql在头文件或源文件中使用 Qt SQL 模块中的类&#xff0c;可以使用包含语句&#xff1a; #include <QtSql>这样会将某个 Qt SQL 模块中的所有类…...

PVE相关名词通俗表述方式———多处细节实验(方便理解)

PVE设置初期&#xff0c;对CIDR、 网关、 LinuxBridge、VLAN等很有困惑的朋友一定很需要一篇能够全面通俗易懂的方式去理解PVE 中Linux网桥的工作方式&#xff0c;就像操作一个英雄&#xff0c;多个技能&#xff0c;还是需要一点点去学习理解的&#xff0c;如果你上来就对着别人…...

C语言实现冒泡排序:从基础到优化全解析

一、什么是冒泡排序&#xff1f; 冒泡排序&#xff08;Bubble Sort&#xff09;是一种经典的排序算法&#xff0c;其工作原理非常直观&#xff1a;通过多次比较和交换相邻元素&#xff0c;将较大的元素“冒泡”到数组的末尾。经过多轮迭代&#xff0c;整个数组会变得有序。 二…...

ReentrantLock(可重入锁) Semaphore(信号量) CountDownLatch

目录 ReentrantLock(可重入锁) &Semaphore(信号量)&CountDownLatchReentrantLock(可重入锁)既然有了synchronized&#xff0c;为啥还要有ReentrantLock?Semaphore(信号量)如何确保线程安全呢&#xff1f;CountDownLatch ReentrantLock(可重入锁) &Semaphore(信号量…...

Zookeeper选举算法与提案处理概览

共识算法(Consensus Algorithm) 共识算法即在分布式系统中节点达成共识的算法&#xff0c;提高系统在分布式环境下的容错性。 依据系统对故障组件的容错能力可分为&#xff1a; 崩溃容错协议(Crash Fault Tolerant, CFT) : 无恶意行为&#xff0c;如进程崩溃&#xff0c;只要…...

Jmeter中的断言

7&#xff09;断言 1--响应断言 功能特点 数据验证&#xff1a;验证响应数据是否包含或不包含特定的字符串、模式或值。多种匹配类型&#xff1a;支持多种匹配类型&#xff0c;如文本、正则表达式、文档等。灵活配置&#xff1a;可以设置多个断言条件&#xff0c;满足复杂的测…...

【通俗理解】隐变量的变分分布探索——从公式到应用

【通俗理解】隐变量的变分分布探索——从公式到应用 关键词提炼 #隐变量 #变分分布 #概率模型 #公式推导 #期望最大化 #机器学习 #变分贝叶斯 #隐马尔可夫模型 第一节&#xff1a;隐变量的变分分布的类比与核心概念【尽可能通俗】 隐变量的变分分布就像是一场“捉迷藏”游戏…...

Vivado程序固化到Flash

在上板调试FPGA时&#xff0c;通常使用JTAG接口下载程序到FPGA芯片中&#xff0c;FPGA本身是基于RAM工艺的器件&#xff0c;因此掉电后会丢失芯片内的程序&#xff0c;需要重新烧写程序。但是当程序需要投入使用时不能每一次都使用JTAG接口下载程序&#xff0c;一般FPGA的外围会…...

铲屎官进,2024年宠物空气净化器十大排行,看看哪款吸毛最佳?

不知道最近换毛季&#xff0c;铲屎官们还承受的住吗&#xff1f;我家猫咪每天都在表演“天女散花”&#xff0c;家里没有一块干净的地方&#xff0c;空气中也都是堆积的浮毛&#xff0c;幸好有宠物空气净化器这种清理好物。宠物空气净化器针对宠物浮毛设计&#xff0c;可以有效…...