💁♂️个人主页 :进击的荆棘
👇作者其它专栏:
|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 🧮数据结构与算法 | 《数据结构与算法》 |
| 算法 | 《算法》 |
| C++ | 《C++》 |
| 🐧Linux | 《Linux系统》 |
| MySQL | 《MySQL》 |
目录
1.线程互斥
2.线程同步
3.线程池
4.线程安全和重入问题
5.常见锁概念
6.STL,智能指针和线程安全
7.其它常见的各种锁
3.线程池
结合之前做的所有封装,进行线程池的设计。做如下准备:
●准备线程的封装
●准备锁和条件变量的封装
●引入日志,对线程进行封装
3.1日志与策略模式
什么是设计模式
IT行业涌入的人太多,两级分化越来越严重,为了更好的方便新手解决问题,大佬们针对一些常见的场景,给定了一些对应的解决方案,这个就是设计模式
日志模式
计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题并支持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要工具。
日志格式一下几个指标是必须有的
●时间戳
●日志等级
以下几个指标是可选的
●文件名行号
●进程,线程相关id信息等
日志有现成的解决方案,如:spdlog、glog、Boost.Log、Log4cxx等,我们采用自定义日志的方式
使用设计模式-策略模式来进行日志的设计
目标日志格式如下:
[可读性很好的时间] [⽇志等级] [进程pid] [打印对应⽇志的⽂件名][⾏号] - 消息内容,⽀持
可变参数
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [17] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [18] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [20] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [21] - hello world
[2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [23] - hello world
Log.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unistd.h>
#include <memory> //智能指针
#include <sstream>
#include <ctime>
#include <filesystem> //C++17
#include "Mutex.hpp"
namespace LogModule
{
using namespace MutexModule;
const std::string gsep = "\r\n";
// 两种策略的基类,1.向屏幕打印,2.向文件打印
class LogStrategy
{
public:
virtual void SyncStrategy(const std::string& message) = 0;
~LogStrategy() = default;
};
class ConsoleLogStrategy : public LogStrategy
{
public:
ConsoleLogStrategy(){}
void SyncStrategy(const std::string& message) override
{
LockGuard lockguard(_mutex);
std::cout << message << gsep;
}
~ConsoleLogStrategy(){}
private:
Mutex _mutex;
};
const std::string defaultpath = "./log";
const std::string defaultname = "my.log";
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string path = defaultpath, const std::string name = defaultname)
: _path(path), _name(name)
{
LockGuard lockguard(_mutex);
//确认路径是否存在
if (std::filesystem::exists(_path))
{
return;
}
try
{
// 创建目录
std::filesystem::create_directories(_path);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cout << "捕获:" << e.what() << gsep;
}
}
void SyncStrategy(const std::string& message) override
{
LockGuard lockguard(_mutex);
std::string filename=_path+(_path.back()=='/'?"":"/")+_name;
std::ofstream out(filename,std::ios::app); //存在,以追加的方式打开;反之,新建
if(!out.is_open()){
return ;
}
out<<message<<gsep;
out.close();
}
~FileLogStrategy(){}
private:
Mutex _mutex;
std::string _path;
std::string _name;
};
//日志等级
enum class Level{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
std::string GetTime(){
//获取当前时间戳
time_t curr=time(nullptr);
struct tm curr_tm;
localtime_r(&curr,&curr_tm);
char buffertime[1024];
snprintf(buffertime,sizeof(buffertime),"%4d %02d %02d %02d:%02d:%02d"
,curr_tm.tm_year+1900 //相对1900年的偏移量
,curr_tm.tm_mon+1 //月0~11
,curr_tm.tm_mday
,curr_tm.tm_hour
,curr_tm.tm_min
,curr_tm.tm_sec
);
return buffertime;
}
std::string LevelToString(Level level){
switch (level)
{
case Level::DEBUG :
return "DEBUG";
case Level::INFO :
return "INFO";
case Level::WARNING :
return "WARNING";
case Level::ERROR :
return "ERROR";
case Level::FATAL :
return "FATAL";
default:
return "UNKNOW";
}
}
class Logger{
public:
Logger(){
EnableConsoleLogStrategy();
}
void EnableConsoleLogStrategy(){
_fflush_strategy=std::make_unique<ConsoleLogStrategy>();
}
void EnableFileLogStrategy(){
_fflush_strategy=std::make_unique<FileLogStrategy>();
}
//内部类,表示未来的一条日志
class LogMessage{
public:
LogMessage(const Level &level,const std::string& src_name,int line,Logger &logger)
:_curr_time(GetTime())
,_level(level)
,_pid(getpid())
,_src_name(src_name)
,_line(line)
,_logger(logger)
{
//将日志的左半部分整合起来
std::stringstream ss;
ss<<"["<<_curr_time<<"] "
<<"["<<LevelToString(_level)<<"] "
<<"["<<_pid<<"] "
<<"["<<_src_name<<"] "
<<"["<<_line<<"] ";
_fileinfo=ss.str();
}
//实现可变参数,os<<"hello"<<"world"<<3.14
template<class T>
LogMessage& operator<<(T message){
std::stringstream ss;
ss<<message;
_fileinfo+=ss.str();
return *this;
}
~LogMessage(){
if(_logger._fflush_strategy){
_logger._fflush_strategy->SyncStrategy(_fileinfo);
}
}
private:
std::string _curr_time;
Level _level;
pid_t _pid;
std::string _src_name;
int _line;
std::string _fileinfo;
Logger &_logger;
};
//故意返回临时变量,临时变量结束时析构,调用真正刷新函数SyncStrategy
LogMessage operator()(Level level,const std::string src_name,int line){
return LogMessage(level,src_name,line,*this);
}
~Logger(){}
private:
std::unique_ptr<LogStrategy> _fflush_strategy;
};
//搞成inline是因为,若多个.cc文件包含Log.hpp可以回导致重复定义
inline Logger log;
#define Log(level) log(level,__FILE__,__LINE__)
#define EnableConsoleLogStrategy() log.EnableConsoleLogStrategy()
#define EnableFileLogStrategy() log.EnableFileLogStrategy()
}
使用样例
cpp
#include "Log.hpp"
#include <iostream>
using namespace LogModule;
int main(){
Log(Level::DEBUG)<<" hello world "<<3.14;
EnableFileLogStrategy();
Log(Level::DEBUG);
return 0;
}
注:
●C++11也已经封装了锁,如std::lock_guard<std::mutex> lock(_mutex>;
3.2线程池设计
线程池:
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护者多个线程,等待监督管理者分配可并发执行的任务。这避免了在处理短时间任务是创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量
线程池的应用场景:
●需要大量的线程来完成任务,且完成任务的时间比较短。如WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,可以想象一个热门网站的点击次数。但对于长时间的任务,如一个Telent连接请求,线程池的优点就不明显了。因为Telent会话时间比线程的创建时间大多了
●对性能要求苛刻的应用,如要求服务器迅速相应客户请求
●接受突发性的大量请求,但不至于是服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误
线程池的种类
a.创建固定数量线程池,循环从任务队列中获取任务对象,获取到任务对象后,执行任务对象中的任务接口
b.浮动线程池,其它同上
我们选择固定线程个数的线程池

ThreadPool.hpp
cpp
#pragma once
#include <vector>
#include <pthread.h>
#include <queue>
#include "Mutex.hpp"
#include "Cond.hpp"
#include "Thread.hpp"
#include "Log.hpp"
namespace ThreadPoolModule
{
using namespace MutexModule;
using namespace ThreadModule;
using namespace LogModule;
const int gnum = 5;
template <class T>
class ThreadPool
{
private:
void WakeUpAll(){
//必须加锁,1.防止唤醒丢失 2.消除数据竞争(_sleepnnum持锁读/写)
LockGuard lockguard(_mutex);
//或者也可以删除_sleepnum的判断,BroadCast的空唤醒开销极小,此时也不用考虑数据竞争的问题
if(_sleepnum)
_cond.BroadCast();
Log(Level::DEBUG)<<"唤醒所有休眠线程";
}
void WakeUpOne(){
_cond.Signal();
Log(Level::DEBUG)<<"唤醒一个线程";
}
public:
ThreadPool(int num = gnum) : _num(num), _isrunning(false),_sleepnum(0)
{
for (int i = 0; i < _num; i++)
{
_threads.emplace_back(
[this](){
HandlerTask();
});
}
};
void Start()
{
if (_isrunning)
return;
_isrunning = true;
for (auto &thread : _threads)
{
thread.Start();
Log(Level::DEBUG) << "start new thread success";
}
}
void Stop(){
if(!_isrunning) return ;
_isrunning=false;
//唤醒所有进程
WakeUpAll();
}
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, 128);
while (true)
{
T t;
{
LockGuard lockguard(_mutex);
if (_task.empty()&&_isrunning)
{
_sleepnum++;
_cond.Wait(_mutex);
_sleepnum--;
}
if(!_isrunning&&_task.empty()){
Log(Level::INFO)<<name<<" 退出线程池";
break;
}
// 此时一定有任务
t = _task.front();
_task.pop();
}
t();
}
}
void EnQueue(const T& in){
if(_isrunning){
LockGuard lockguard(_mutex);
_task.push(in);
//若所有线程都已休眠,则压入任务毫无意义
//且会卡在_cond.Wait()这一步
if(_threads.size()==_sleepnum){
WakeUpOne();
}
}
}
void Join(){
for(auto &thread:_threads){
thread.Join();
}
}
~ThreadPool() {};
private:
std::vector<Thread> _threads;
int _num;
std::queue<T> _task;
Mutex _mutex;
Cond _cond;
bool _isrunning;
int _sleepnum;
};
}
g++ main.cc -std=c++17 -lpthread // 需要使⽤C++17
3.3线程安全的单例模式
3.3.1什么是单例模式
保证一个类在整个程序中只有一个实例 ,并提供一个全局访问点来获取它
3.3.2单例模式的特点
某些类,只应该具有一个对象(实例),就称之为单例
例如,每个人都只应有一个配偶
在很多服务器开发场景中,经常需要让服务器加载很多的数据(上百G)到内存中。此时往往要用一个单例的类来管理这些数据
3.3.3饿汉实现方式和懒汉实现方式
洗碗的例子
吃完饭,立刻洗碗,这种就是饿汉方式,因为下一顿吃的时候可以立刻拿着碗就能吃饭
吃完饭,先把碗放下,然后下一顿饭用到这个碗了再洗碗,就是懒汉方式
懒汉方式最核心的思想是"延时加载",从而能够优化服务器的启动速度
3.3.4饿汉方式实现单例模式
cpp
template <typename T>
class Singleton
{
static T data;
public:
static T *GetInstance()
{
return &data;
}
};
只要通过Singleton这个包装类来使用T对象,则一个进程中只有一个T对象的实例
3.3.5懒汉方式实现单例模式
cpp
template <typename T>
class Singleton
{
static T *inst;
public:
static T *GetInstance()
{
if (inst == NULL)
{
inst = new T();
}
return inst;
}
};
存在一个严重的问题,线程不安全。
第一次调用给你GetInstance的时候,若两个线程同时调用,可能会创建出两份T对象的实例
但后续再次调用,就没有问题
3.3.6懒汉方式实现单例模式(线程安全版本)
cpp
// 懒汉模式, 线程安全
template <typename T>
class Singleton
{
volatile static T *inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
static std::mutex lock;
public:
static T *GetInstance()
{
if (inst == NULL)
{ // 双重判定空指针, 降低锁冲突的概率, 提⾼性能.
lock.lock(); // 使⽤互斥锁, 保证多线程情况下也只调⽤⼀次 new.
if (inst == NULL)
{
inst = new T();
}
lock.unlock();
}
return inst;
}
};
注:
1.加锁解锁的位置
2.双重if判定,避免不必要的锁竞争
3.volatile关键字防止过度优化
3.4单例模式的线程池
ThreadPool.hpp
cpp
#pragma once
#include <vector>
#include <pthread.h>
#include <queue>
#include "Mutex.hpp"
#include "Cond.hpp"
#include "Thread.hpp"
#include "Log.hpp"
namespace ThreadPoolModule
{
using namespace MutexModule;
using namespace ThreadModule;
using namespace LogModule;
const int gnum = 5;
template <class T>
class ThreadPool
{
private:
void WakeUpAll()
{
// 必须加锁,1.防止唤醒丢失 2.消除数据竞争(_sleepnnum持锁读/写)
LockGuard lockguard(_mutex);
// 或者也可以删除_sleepnum的判断,BroadCast的空唤醒开销极小,此时也不用考虑数据竞争的问题
if (_sleepnum)
_cond.BroadCast();
Log(Level::DEBUG) << "唤醒所有休眠线程";
}
void WakeUpOne()
{
_cond.Signal();
Log(Level::DEBUG) << "唤醒一个线程";
}
ThreadPool(int num = gnum) : _num(num), _isrunning(false), _sleepnum(0)
{
for (int i = 0; i < _num; i++)
{
_threads.emplace_back(
[this]()
{
HandlerTask();
});
}
};
void Start()
{
if (_isrunning)
return;
_isrunning = true;
for (auto &thread : _threads)
{
thread.Start();
Log(Level::DEBUG) << "start new thread success";
}
}
//单例模式,只能有一个对象
//所以,禁掉拷贝构造和赋值构造
ThreadPool(const ThreadPool<T> &)=delete;
ThreadPool<T> &operator=(const ThreadPool<T> &)=delete;
public:
// 懒汉模式的单例
static ThreadPool<T> *GetInstance()
{
// 为防止inc已经new好,但后续调用函数时仍申请锁
// 可以再加一个判断
if (inc == nullptr)
{
LockGuard lockguard(_lock);
Log(Level::INFO) << "获取单例";
if (inc == nullptr)
{
inc = new ThreadPool<T>();
Log(Level::INFO)<<"首次使用单例,获取";
inc->Start();
}
}
return inc;
}
//单例模式只能启停一次,要么明确stop后不能重启,Join后重置isrunning=false
void Stop()
{
if (!_isrunning)
return;
_isrunning = false;
// 唤醒所有进程
WakeUpAll();
}
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, 128);
while (true)
{
T t;
{
LockGuard lockguard(_mutex);
while (_task.empty() && _isrunning)
{
_sleepnum++;
_cond.Wait(_mutex);
_sleepnum--;
}
if (!_isrunning && _task.empty())
{
Log(Level::INFO) << name << " 退出线程池&&任务队列为空";
break;
}
// 此时一定有任务
t = _task.front();
_task.pop();
}
t();
}
}
void EnQueue(const T &in)
{
if (_isrunning)
{
LockGuard lockguard(_mutex);
_task.push(in);
// 若所有线程都已休眠,则压入任务毫无意义
// 且会卡在_cond.Wait()这一步
if (_threads.size() == _sleepnum)
{
WakeUpOne();
}
}
}
//Join也不能两次
void Join()
{
for (auto &thread : _threads)
{
thread.Join();
}
}
~ThreadPool() {};
private:
std::vector<Thread> _threads;
int _num;
std::queue<T> _task;
Mutex _mutex;
Cond _cond;
bool _isrunning;
int _sleepnum;
// 采用单例模式实现线程池
static ThreadPool<T> *inc;
//为什么还需要一把锁?因为它与_mutex保护的对象不同,一个是普通成员,一个是静态指针
//关键是,使用锁的时候,_mutex根本就没有创建
static Mutex _lock;
};
// 类内static初始化应在类外
template <class T>
ThreadPool<T> *ThreadPool<T>::inc = nullptr;
template <class T>
Mutex ThreadPool<T>::_lock;
}
测试样例
cpp
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include <unistd.h>
using namespace LogModule;
using namespace ThreadPoolModule;
int main(){
EnableConsoleLogStrategy();
// ThreadPool<task_t> *tp = new ThreadPool<task_t>();
// tp->Start();
// sleep(3);
int cnt=5;
while(cnt--){
// tp->EnQueue(DownLoad);
ThreadPool<task_t>::GetInstance()->EnQueue(DownLoad);
sleep(1);
}
// tp->Stop();
ThreadPool<task_t>::GetInstance()->Stop();
// tp->Join();
ThreadPool<task_t>::GetInstance()->Join();
// Log(Level::DEBUG)<<" hello world "<<3.14;
// EnableFileLogStrategy();
// Log(Level::DEBUG);
}
cpp
[2026 11 08 14:28:29] [INFO] [902175] [ThreadPool.hpp] [72] 获取单例
[2026 11 08 14:28:29] [INFO] [902175] [ThreadPool.hpp] [77] 首次使用单例,获取
[2026 11 08 14:28:29] [DEBUG] [902175] [ThreadPool.hpp] [54] start new thread success
[2026 11 08 14:28:29] [DEBUG] [902175] [ThreadPool.hpp] [54] start new thread success
[2026 11 08 14:28:29] [DEBUG] [902175] [ThreadPool.hpp] [54] start new thread success
[2026 11 08 14:28:29] [DEBUG] [902175] [ThreadPool.hpp] [54] start new thread success
[2026 11 08 14:28:29] [DEBUG] [902175] [ThreadPool.hpp] [54] start new thread success
[2026 11 08 14:28:29] [INFO] [902175] [Task.hpp] [9] 执行下载任务
[2026 11 08 14:28:30] [DEBUG] [902175] [ThreadPool.hpp] [33] 唤醒一个线程
[2026 11 08 14:28:30] [INFO] [902175] [Task.hpp] [9] 执行下载任务
[2026 11 08 14:28:31] [DEBUG] [902175] [ThreadPool.hpp] [33] 唤醒一个线程
[2026 11 08 14:28:31] [INFO] [902175] [Task.hpp] [9] 执行下载任务
[2026 11 08 14:28:32] [DEBUG] [902175] [ThreadPool.hpp] [33] 唤醒一个线程
[2026 11 08 14:28:32] [INFO] [902175] [Task.hpp] [9] 执行下载任务
[2026 11 08 14:28:33] [DEBUG] [902175] [ThreadPool.hpp] [33] 唤醒一个线程
[2026 11 08 14:28:33] [INFO] [902175] [Task.hpp] [9] 执行下载任务
[2026 11 08 14:28:34] [DEBUG] [902175] [ThreadPool.hpp] [28] 唤醒所有休眠线程
[2026 11 08 14:28:34] [INFO] [902175] [ThreadPool.hpp] [111] thread-3 退出线程池&&任务队列为空
[2026 11 08 14:28:34] [INFO] [902175] [ThreadPool.hpp] [111] thread-4 退出线程池&&任务队列为空
[2026 11 08 14:28:34] [INFO] [902175] [ThreadPool.hpp] [111] thread-2 退出线程池&&任务队列为空
[2026 11 08 14:28:34] [INFO] [902175] [ThreadPool.hpp] [111] thread-5 退出线程池&&任务队列为空
[2026 11 08 14:28:34] [INFO] [902175] [ThreadPool.hpp] [111] thread-1 退出线程池&&任务队列为空
4.线程安全和重入问题
概念
**线程安全:**就是多个线程再访问共享资源时,能够正确地执行,不好相互干扰或破坏彼此的执行结果。一般而言,多个线程并发同一段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或静态变量进行操作,并且没有锁保护的情况下,容易出现问题
**重入:**同一个函数被不同的执行流调用,当前一个流程还没执行完,就有其它的执行流再次进入,称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或任何问题,则该函数被称为可重入函数,否则,是不可重入函数
重入可以分为两种:
●多线程重入函数
●信号导致一个执行流重复进入函数
常见的线程不安全情况
●不保护共享变量的函数
●函数状态随着被调用,状态发生变化的函数
●返回指向静态变量指针的函数
●调用线程不安全函数的函数
常见线程安全的情况
●每个线程对全局变量或静态变量只有读取的权限而没有写入的权限,一般来说这些线程是安全的
●类或者接口对于线程来说都是原子操作
●多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况
●调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
●调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
●可重入函数体内使用了静态的数据结构
常见可重入的情况
●不使用全局变量或静态变量
●不使用malloc或new开辟出的空间
●不调用不可重入函数
●不返回静态或全局数据,所有数据都由函数的的调用者提供
●使用本地数据,或通过制作全局数据的本地拷贝来保护全局数据
结论
其实上面说的都是一回事
可重入与线程安全联系
●函数是可重入的,那就是线程安全的(知道这一句话就够了)
●函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
●若一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的
可重入与线程安全区别可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的
若将对临界资源的访问加上锁,则这个函数是线程安全的,但若这个重入函数的锁还未释放则会产生死锁,因此是不可重入的
注:●若不考虑信号导致一个执行流重复进入函数这种情况,线程安全和重入在安全角度不做区分
●但是线程安全侧重说明线程访问公共资源的安全情况,表现的是并发线程的特点
●可重入描述的是一个函数是否能被重复进入,表示的是函数的特点
5.常见锁概念
5.1死锁
●死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其它进程所占用不会释放的资源而处于的一种永久等待状态
●假设现在线程A,线程B必须同时持有锁1和锁2,才能进行后续资源的访问

申请一把锁是原子的,但申请两把锁就不一定了

造成的结果是

5.2死锁的4个必要条件
●互斥条件:一个资源每次只能被一个执行流使用
●请求与保持条件:一个执行流因请求资源而阻塞时,对以获取的资源保持不放

●**不剥夺条件:**一个执行流已获取的资源,在未使用完之前,不能强行剥夺

●**循环等待条件:**若干执行流之间形成一种头尾相接的循环等待资源的关系

5.3避免死锁
●破坏死锁的四个必要条件
▢破坏循环等待条件问题:资源一次性分配,使用超时机制、加锁顺序一致
cpp
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <unistd.h>
// 定义两个共享资源(整数变量)和两个互斥锁
int shared_resource1 = 0;
int shared_resource2 = 0;
std::mutex mtx1, mtx2;
// ⼀个函数,同时访问两个共享资源
void access_shared_resources()
{
// std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
// std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
// // 使⽤ std::lock 同时锁定两个互斥锁
// std::lock(lock1, lock2);
// 现在两个互斥锁都已锁定,可以安全地访问共享资源
int cnt = 10000;
while (cnt)
{
++shared_resource1;
++shared_resource2;
cnt--;
}
// 当离开 access_shared_resources 的作⽤域时,lock1 和 lock2 的析构函数会
//被⾃动调⽤
// 这会导致它们各⾃的互斥量被⾃动解锁
}
// 模拟多线程同时访问共享资源的场景
void simulate_concurrent_access()
{
std::vector<std::thread> threads;
// 创建多个线程来模拟并发访问
for (int i = 0; i < 10; ++i)
{
threads.emplace_back(access_shared_resources);
}
// 等待所有线程完成
for (auto &thread : threads)
{
thread.join();
}
// 输出共享资源的最终状态
std::cout << "Shared Resource 1: " << shared_resource1 << std::endl;
std::cout << "Shared Resource 2: " << shared_resource2 << std::endl;
}
int main()
{
simulate_concurrent_access();
return 0;
}
●避免锁未释放的场景
5.4避免死锁算法
●死锁检测算法
●银行家算法
6.STL,智能指针和线程安全
6.1STL中的容器是否是线程安全的?
不是
因为,STL的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。
而且对于不同的容器,加锁方式不同,性能可能也不同(如hash表的锁表和锁桶)
因此STL默认不是线程安全,若需要在多线程环境下使用,往往需要调用者自行保证线程安全
6.2智能指针是否是线程安全的?
对于unique_ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题
对于shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题,但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证shared_ptr能够高效,原子的操作引用计数
7.其它常见的各种锁
●悲观锁:在每次取数据是,总是担心数据会被其它线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其它线程想要访问数据时,被阻塞挂起
●乐观锁:每次取数据时候,总是乐观的认为数据不会被其它线程修改,因此不上锁。但是在更新数据前,会判断其它数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作
CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。若相等则用新值更新。反之,则失败,失败就重试,一般是一个自旋的过程,即不断重试
●自旋锁,读写锁