本篇是线程同步与互斥系列最后一篇文章;此篇文章讲会介绍相关日志的设计代替我们日常使用的printf/cout等等;以及利用之前巧妙封装的cond/mutex/threads等文件来实现单例线程池;以及线程安全与重入/死锁问题介绍与分析等以及一些线程安全问题如STL容器智能指针等等;欢迎阅读!!!
博主主页:☛☛☛羑悻的小杀马特.-CSDN博客 ☚☚☚ ☺
欢迎拜访:羑悻的小杀马特.-CSDN博客本篇主题:秒懂百科之探究Linux线程同步与互斥第二讲
制作日期:2025.06.23
隶属专栏:linux之旅
目录
一·日志设计:
1.1何为日志:
计算机中的⽇志是记录系统和软件运⾏中发⽣事件的⽂件,主要作⽤是监控运⾏状态、记录异常信息,帮助快速定位问题并⽀持程序员进⾏问题修复。它是系统维护、故障排查和安全管理的重要⼯具。
一个日志一般包括:时间戳; ⽇志等级; ⽇志内容; ⽂件名⾏号 ;进程;线程相关id信息等
其中日志等级:日志是衡量程序健康状态的标志;我们下面自定义设计的日志等级:
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
1.2自定义日志实现:
那么,下面我们就自己来实现自己的日志:
思路:
我们这里采取的是继承+多态的形式设计的刷新策略(文件或者控制台[我们一般常用]);
然后就是调用日志的形式:
比如我们想让它打印出来是这个样子:
对于参数的话就模仿这个设计;然后这里我们做了一个规定(输入):
这里我们通过调用日志类重载的()来输入日志等级然后进行重载的<<进行读取信息进行打印:
设计思路: 首先让我们的日志类LOG支持重载();然后比如我们输入了多行日志;它会依次打印-->为了能实现这种模式:我们设计了一个LOG的内部类:Logmess负责读取信息进行调用输出策略;把它设计成内部类的原因(当我们调用LOG的重载的()此时输出的就是logmess的匿名对象;然后我们让它支持了多模版的<<输入;之后把析构改成调用刷新策略即可;每行调用的LOG()后自动会析构然后调用对应刷新策略)-->这里我们就实现了上面所说的。
利用特性:
1·匿名对象声明周期默认在当前行;过完了;立刻析构。
2·内部类默认为外部类的友好类;可以访问外部类的所有变量或者函数(logmess调用log的成员变量(刷新策略指针)) 。
3·时间戳/智能指针/预处理宏替换/枚举类的使用:枚举类特点简记:作用域限制/隐式转换问题等。
4·继承与多态。
这些特性的使用见代码注释即可。
log.hpp:
#ifndef __LOG__
#define __LOG__
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include <memory>
#include <fstream>
#include <filesystem>
#include <sstream>
#include<ctime>
#include<cstdio>
#include "mutex.hpp"
using namespace std;
#define gsep "\r\n"
// 基类:
class Logstrategy
{
public:
Logstrategy() {}
virtual void synclog(const string &message) = 0;
~Logstrategy() {}
};
// 控制台打印日志:
class consolelogstrategy : public Logstrategy
{
public:
consolelogstrategy() {}
void synclog(const string &message) override
{
// 加锁完成多线程互斥:
{
mutexguard md(_mutex);
cout << message << gsep;
}
}
~consolelogstrategy() {}
private:
mutex _mutex;
};
// 自定义文件打印日志:
const string P = "./log";
const string F = "my.log";
class fileLogstrategy : public Logstrategy
{
public:
fileLogstrategy(const string path = P, const string file = F) : _path(path), _file(file)
{
// 如果指定路径(目录)不存在进行创建;否则构造直接返回:
{
mutexguard md(_mutex);
if (filesystem::exists(_path))
return;
try
{
filesystem::create_directories(_path);
}
catch (filesystem::filesystem_error &e)
{
cout << e.what() << gsep;
}
}
}
void synclog(const string &message) override
{
// 得到指定文件名:
{
mutexguard md(_mutex);
string name = _path + (_path.back() == '/' ? "" : "/") + _file;
// 打开文件进行<<写入:
ofstream out(name, ios::app); // 对某文件进行操作的类对象
if (!out.is_open())
return; // 成功打开
out << message << gsep;
out.close();
}
}
~fileLogstrategy() {}
private:
string _path;
string _file;
mutex _mutex;
};
// 用户调用日志+指定打印:
// 日志等级:
enum class loglevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
// 完成枚举值对应由数字到原值转化:
string trans(loglevel &lev)
{
switch (lev)
{
case loglevel::DEBUG:
return "DEBUG";
case loglevel::INFO:
return "INFO";
case loglevel::WARNING:
return "WARNING";
case loglevel::ERROR:
return "ERROR";
case loglevel::FATAL:
return "FATAL";
default:
return "ERROR";
}
return"";
}
// 从时间戳提取出当前时间:
string gettime()
{
time_t curtime=time(nullptr);
struct tm t;
localtime_r(&curtime,&t);
char buff[1024];
sprintf(buff,"%4d-%02d-%02d %02d:%02d:%02d",
t.tm_year+1900,//注意struct tm成员性质
t.tm_mon+1,
t.tm_mday,
t.tm_hour,
t.tm_min,
t.tm_sec
);
return buff;
}
class Log
{
public:
// Log刷新策略:
void console() { _fflush_strategy = make_unique<consolelogstrategy>(); }
void file() { _fflush_strategy = make_unique<fileLogstrategy>(); }
Log()
{
// 默认是控制台刷新:
console();
}
// 我们想让一个类重载了<<支持连续的<<输入并且希望每行结束就进行刷新;因此这个meesage类
// 析构就可以执行刷新;-->内部类天然就是外部类的友元类;可以访问外部类所有成员变量及函数
class Logmess
{
public:
Logmess(loglevel &lev, string filename, int line, Log &log) : _lev(lev),
_time(gettime()), _pid(getpid()), _filename(filename), _log(log), _linenum(line)
{
stringstream ss;
ss << "[" << _time << "] "
<< "[" << trans(_lev) << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _linenum << "] "
<< "<--> ";
_mergeinfo=ss.str();
}
template<class T>
Logmess& operator <<(const T& data){
stringstream ss;
ss<<data;
_mergeinfo +=ss.str();
return *this;
}
~Logmess()
{
_log._fflush_strategy->synclog(_mergeinfo);
}
private:
loglevel _lev;
string _time;
pid_t _pid;
string _filename;
int _linenum;
string _mergeinfo;
Log &_log;
};
Logmess operator()(loglevel l,string f,int le)
{ //返回的是匿名对象(临时对象)-->也就是作用完当前行
//(执行完logmess的<< <<后自动调用logmess的析构也就是直接策略打印)
return Logmess(l,f,le,*this);
}
~Log() {}
private:
unique_ptr<Logstrategy> _fflush_strategy;
};
Log l;
#define use_log(x) l(x,__FILE__,__LINE__)//自动判断是哪行哪个文件
#define filestrategy l.file()
#define consolestrategy l.console()
#endif
main.cc:
#include"log.hpp"
int main(){
// unique_ptr< Logstrategy> up=make_unique<consolelogstrategy>();
// up->synclog("hahaha");
// unique_ptr< Logstrategy> up=make_unique<fileLogstrategy>();
// up->synclog("hahaha");
consolestrategy ;
filestrategy ;
use_log(loglevel::DEBUG)<<1<<"日志学习";
}
这里我们使用了多个define;这里调用写起来还方便点!!!
控制台输出测试:
文件输出测试:
二·线程池设计:
首先先介绍何为单例模式然后实现单例式的线程池代码。
2.1单例模式:
2.1.1懒汉模式饿汉模式介绍:
简答解释:某些类,只应该具有⼀个对象(实例)就称之为单例。
在很多服务器开发场景中,经常需要让服务器加载很多的数据(上百G)到内存中.此时往往要⽤⼀个单例的类来管理这些数据。
形象理解:
吃完饭, ⽴刻洗碗, 这种就是饿汉⽅式. 因为下⼀顿吃的时候可以⽴刻拿着碗就能吃饭。
吃完饭, 先把碗放下, 然后下⼀顿饭⽤到这个碗了再洗碗, 就是懒汉⽅式。
懒汉⽅式最核⼼的思想是"延时加载".从⽽能够优化服务器的启动速度.
我们一般使用的是懒汉模式的单例(比如malloc原理其实就利用了这个特性) 。
2.1.2懒汉模式饿汉模式代码实现:
饿汉:
template <typename T>
class Singleton {
static T data;
public:
static T* GetInstance() {
return &data;
}
};
说白了就是只要这个类被实现了;自然就会开辟好一个对象;然后每次我们只能调用这个接口获得这个已经存在的对象。
懒汉:
template <typename T>
class Singleton {
static T* inst;
public:
static T* GetInstance() {
if (inst == NULL) {
inst = new T();
}
return inst;
}
};
这里;我们先不定义对象;而是等到想要这个对象的时候那这个指针去new一个(才开辟好空间);然后如果在去调用这个接口就直接拿到这个指针即可。
注:我们是常使用懒汉模式的;但是这里有判断(非原子性)-->线程不安全;因此我们又选择了加锁;但是这样效率可能会低一点:(假设一个线程很快完成单例化;然后后面的一群线程正好来了;如果没有双层判断;就会阻塞一个个发现不是空返回_ins;非常慢;为了提高效率这样就不用加锁在一个个判断了还能保证线程安全。);因此引入双重判断!
优化懒汉:
// 懒汉模式, 线程安全
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;
}
};
加锁解锁的位置.
双重if判定,避免不必要的锁竞争.
volatile关键字防⽌过度优化.
2.2单例式线程池实现:
下面我们说一下大概思路:
首先我们还是复用了之前封装的mutex/cond/threads这些hpp;然后来实现的线程池;对于线程池:我们可以理解为主线程在不断给它里面放任务;然后子线程就是处于循环取任务;无任务就去cond休眠;有任务就去执行;对于放任务的时候判断是否没有线程了然后进行唤醒操作。
重点来了:
线程池什么时候终止;换句话说只要我们stop后它就会终止吗?
线程池终止条件:主线程调用了stop并且任务队列没任务了故所有子线程执行完退出被主线程join。
那么该如何实现:
我们调用stop只不过是一个标记;然后我们要去看队列还有没有任务:
1·没任务子线程都去cond等待了:此时就需要全部唤醒;然后等待执行完发现没任务+标记就退出!
2·有任务有子线程执行;有子线程等待:此时执行完没任务了+标记也退出!
3·有任务子线程全部等待:此时必须全部唤醒;不然一直阻塞;后面同理!
4·有任务子线程全部在执行:此时子线程直接执行完+标记直接退即可!
因此我们只要明白这几点;然后根据它进行设计就行了!!!
后面我们就直接试下线程池了;一些细节问题还是没有展开说;可以具体看代码处注释:
cond.hpp:
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <unistd.h>
#include <vector>
#include <pthread.h>
#include "mutex.hpp"
class cond
{
public:
cond() { pthread_cond_init(&_cond, nullptr); }
void Wait(mutex &mx)
{
int n = pthread_cond_wait(&_cond, mx.getmutex());
(void)n;
}
void notify()
{
int n = pthread_cond_signal(&_cond);
(void)n;
}
void allnotify()
{
int n = pthread_cond_broadcast(&_cond);
(void)n;
}
~cond() { pthread_cond_destroy(&_cond); }
private:
pthread_cond_t _cond;
};
mutex.hpp:
#pragma once
#include<pthread.h>
//封装锁:
class mutex{
public:
mutex(){
int n= pthread_mutex_init(&_mutex,nullptr);
(void)n;
}
void Lock(){ pthread_mutex_lock(&_mutex);}
void Unlock(){ pthread_mutex_unlock(&_mutex);}
pthread_mutex_t*getmutex(){return &_mutex;}
~mutex(){
int n= pthread_mutex_destroy(&_mutex);
(void)n;
}
private:
pthread_mutex_t _mutex;
};
//自动上锁与解锁
class mutexguard{
public:
//初始化为上锁;
mutexguard(mutex &mg):_mg(mg){ _mg.Lock() ; }//引用
//析构为解锁:
~mutexguard(){_mg.Unlock() ; }
private:
mutex &_mg;//注意引用:确保不同线程上锁与解锁的时候拿到同一把锁;不能是直接赋值
};
thread.hpp:
#ifndef THREAD_H
#define THREAD_H
#include <iostream>
#include <string>
#include <pthread.h>
#include <cstdio>
#include <cstring>
#include <functional>
#include<unistd.h>
#include<vector>
#include<queue>
using namespace std;
namespace td
{
static uint32_t num=1;
class Thread
{
using func_t = function<void()>;
public:
Thread(func_t func) : _tid(0), _res(nullptr),
_func(func), _isrunning(false),
_isdetach(false)
{
_name="Thread-"+to_string(num++);
}
static void *Routine(void *arg){
Thread *self =static_cast<Thread*>(arg);
//需要查看是否进行了start前的detach操作:
pthread_setname_np(self->_tid, self->_name.c_str());
cout<<self->_name.c_str()<<endl;
self->_isrunning=1;
if(self->_isdetach) pthread_detach(self->_tid);
self->_func();
return nullptr;
}
bool start(){
if(_isrunning) return false;
int n = pthread_create(&_tid, nullptr, Routine, this);
if (n != 0)
{
//cerr << "create thread error: " << strerror(n) << endl;
return false;
}
else
{
//cout << _name << " create success" << endl;
return true;
}
}
bool stop(){
if(_isrunning){
int n= pthread_cancel(_tid);
if (n != 0)
{
//cerr << "cancel thread error: " << strerror(n) << endl;
return false;
}
else
{
_isrunning = false;
// cout << _name << " stop" << endl;
return true;
}
}
return false;
}
bool detach(){
if(_isdetach) return false;
if(_isrunning)pthread_detach(_tid);//创建成功的线程进行分离操作
_isdetach=1;//未创线程进行分离只进行标记
return true;
}
bool join(){
if(_isdetach) {
// cout<<"线程 "<<_name<<"已经被分离;不能进行join"<<endl;
return false;
}
//只考虑运行起来的线程了:
int n = pthread_join(_tid, &_res);
if (n != 0)
{
//std::cerr << "join thread error: " << strerror(n) << std::endl;
}
else
{
//std::cout << "join success" << std::endl;
}
return true;
}
~Thread() {}
private:
pthread_t _tid;
string _name;
void *_res;
func_t _func;
bool _isrunning;
bool _isdetach;
};
}
#endif
log.hpp:
这里直接看上面实现的即可!!!
threadpool.hpp:
#pragma once
#include "log.hpp"
#include "cond.hpp"
#include "thread.hpp"
using namespace td;
const int N = 5;
template <class T>
class Threadpool
{
private:
Threadpool(int num = N) : _size(num)
{
for (int i = 0; i < _size; i++)
{
_threads.push_back(Thread([this]()
{ this->handletask(); }));
}
}
// 单例只允许实例出一个对象
Threadpool(const Threadpool<T> &t) = delete;
Threadpool<T> &operator=(const Threadpool<T> &t) = delete;
void Start()
{
if (_isrunning)//勿忘标记位
return;
_isrunning = true;
for (int i = 0; i < _size; i++)
{
use_log(loglevel::DEBUG) << "成功启动一个线程";
;
_threads[i].start();
}
}
public:
static Threadpool<T> *getinstance()//必须采用静态(不创建对象的前提下进行获得类指针)
{
if (_ins == nullptr) //双重判断-->假设一个线程很快完成单例化;然后后面的一群线程正好来了;如果没有双层判断;就会阻塞一个个发现不是空返回_ins;
//非常慢;为了提高效率这样就不用加锁在一个个判断了还能保证线程安全。
{
{
mutexguard mg(_lock);//静态锁;
if (_ins == nullptr)
{
_ins = new Threadpool<T>();
use_log(loglevel::DEBUG) << "创建一个单例";
_ins->Start();//创建单例自启动
}
}
}
use_log(loglevel::DEBUG) << "获得之前创建的一个单例";
return _ins;
}
void stop()//不能立刻停止如果队列有任务还需要线程完成完然后从handl函数退出即可
{
mutexguard mg(_Mutex);//这里为了防止多线程调用线程池但是单例化杜绝了这点
if (_isrunning)
{
_isrunning = 0;//因此只搞个标记
use_log(loglevel::DEBUG) << "唤醒所有线程";
//被唤醒后没有抢到锁的线程虽然休眠但是不用再次唤醒了;os内设它就会让它所只要出现就去竞争
_Cond.allnotify();//万一还有其他线程还在休眠就需要都唤醒-->全部子线程都要退出
}
return;
}
void join()
{
// mutexguard mg(_Mutex);这里不能加锁;如果join的主线程快的话;直接就拿到锁了
// 即使唤醒子线程;他们都拿不到锁故继续休眠等待锁;而主线程join这一直拿着 锁等子线程
// 故造成了---->死锁问题
// 但是可能出现多线程同时访问;后面把它设置单单例模式就好了
use_log(loglevel::DEBUG) << "回收线程";
for (int i = 0; i < _size; i++)
_threads[i].join();
}
bool equeue(const T &tk)
{
mutexguard mg(_Mutex);
if (_isrunning)
{
_task.push(tk);
if (_sleepingnums == _size)
_Cond.notify(); // 全休眠必须唤醒一个执行
use_log(loglevel::DEBUG) << "成功插入一个任务并唤醒一个线程";
return true;
}
return false;
}
void handletask()
{ // 类似popqueue
char name[128];//在线程局部存储开;不用加锁非全局就行
pthread_getname_np(pthread_self(), name, sizeof(name));
while (1)
{
T t;
{
mutexguard gd(_Mutex);
while (_task.empty() && _isrunning)//休眠条件
{
_sleepingnums++;
_Cond.Wait(_Mutex);
_sleepingnums--;
// cout<<1<<endl;
}
if (_task.empty() && !_isrunning)//醒来后发现符合退出条件就退出
{
use_log(loglevel::DEBUG) << name << "退出";
break; // 代码块执行完了;锁自动释放
}
t = _task.front();
_task.pop();
}
t();
}
}
~Threadpool() {}
private:
vector<Thread> _threads;
int _size;
mutex _Mutex;
cond _Cond;
queue<T> _task;
bool _isrunning;
int _sleepingnums;
//仅仅只是声明
static Threadpool<T> *_ins;
static mutex _lock;
};
//类内声明内外定义初始化
template<class T>
Threadpool<T>*Threadpool<T> ::_ins=nullptr;
template<class T>
mutex Threadpool<T> ::_lock;
main.cc:
#include "threadpool.hpp"
#include<unistd.h>
void print(){
cout<<"打印中..."<<endl;
sleep(1);
}
void download(){
cout<<"下载中..."<<endl;
sleep(1);
}
void getlog(){
cout<<"获得日志中..."<<endl;
sleep(1);
}
void browse(){
cout<<"浏览中..."<<endl;
sleep(1);
}
using func_t=function<void()>;
vector<func_t>task;
int cnt=0;
int main(){
//初始化任务:
task.push_back( print);
task.push_back( download);
task.push_back( getlog);
task.push_back(browse);
//非单例模式测试:
// Threadpool<func_t>tp;
// tp.Start();
// int t=10;
// while(t--){
// tp.equeue(task[cnt++]);
// cnt%=task.size();
// }
// tp.stop();
// tp.join();
// //只要有子线程处于cond mutex等阻塞状态;主线程就不会退出;
// //会一直等着子线程都执行完才退出;其他状态比如子线程还在执行任务的时候;人为退出主线程那么此时;子线程也会挂掉程序段错误终止
// sleep(5);
//单例模式测试:
int i=5;
while(i--){
Threadpool<func_t>::getinstance()->equeue(task[cnt++]);
cnt%=task.size();
}
Threadpool<func_t>::getinstance()->stop();
Threadpool<func_t>::getinstance()->join();
}
这里博主在测试的时候发现个小bug:
只要有子线程处于cond mutex等阻塞状态;主线程就不会退出;会一直等着子线程都执行完才退出;其他状态比如子线程还在执行任务的时候;认为退出主线程那么此时;子线程也会挂掉程序段错误终止!
测试效果:
和期待的大差不大!!!
三·线程安全和重入问题:
3·1线程安全:
就是多个线程在访问共享资源时,能够正确地执行,不会相互干扰或破坏彼此的执行结果。一般而言,多个线程并发同一段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进行操作,并且没有锁保护的情况下,容易出现该问题。
总之,就是多线程同时访问共享资源就有可能不安全(加锁等等处理)!
常⻅的线程不安全的情况:
①不保护共享变量的函数 !
②函数状态随着被调⽤!
③状态发⽣变化的函数,返回指向静态变量指针的函数!
④调⽤线程不安全函数的函数!
常⻅的线程安全的情况:
①每个线程对全局变量或者静态变量只有读取的权限,⽽没有写⼊的权限,⼀般来说这些线程是安全的!
②类或者接⼝对于线程来说都是原⼦操作!
③多个线程之间的切换不会导致该接⼝的执⾏结果存在⼆义性!
3.2重入:
同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
总之,支持多线程同时进入不会发生问题的函数!
到现在;我们理解的重入可以分为两种:
1·多线程重入函数!
2·信号导致一个执行流重复进入函数!-->可能导致死锁(拿着锁处理信号;自己又被锁住)!
常⻅不可重⼊的情况:
①调⽤了malloc/free函数,因为malloc函数是⽤全局链表来管理堆的!
②调⽤了标准I/O库函数,标准I/O库的很多实现都以不可重⼊的⽅式使⽤全局数据结构!
③可重⼊函数体内使⽤了静态的数据结构!
常⻅可重⼊的情况:
①不使⽤全局变量或静态变量!
②不使⽤⽤malloc或者new开辟出的空间!③不调⽤不可重⼊函数!
④不返回静态或全局数据,所有数据都有函数的调⽤者提供!
⑤使⽤本地数据,或者通过制作全局数据的本地拷⻉来保护全局数据!
总结下:
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
举个例子;比如带锁的信号函数;它是线程安全的;但是就拿单线程执行来说;首先拿着锁突然去处理信号了保存了自己的锁;也就是物理内存中无锁了﹔接着再次进入这个函数; lock的时候发现物理内存因此就阻塞住-->锁本身就被你拿了;但是自己还阻塞了-->死锁问题-->线程安全!->可重入函数!
如果不考虑信号导致一个执行流重复进入函数 这种重入情况,线程安全和重入在安全角度不做区分!
但是线程安全侧重说明线程访问公共资源的安全情况,表现的是并发线程的特点!
可重入描述的是一个函数是否能被重复进入,表示的是函数的特点!
四·死锁问题:
4.1死锁介绍:
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
形象理解下;就是小明小强要去买糖;但是他俩一人五毛钱;而糖1块钱;因此需要他们凑一起去买;但是谁都不肯让给谁那五毛;都各自拿着!
共享资源-->糖
小明-->线程a
小强-->线程b
如:
申请一把锁是原子的,但是申请两把锁就不一定了因为两把锁的话只持有一把既不算无锁又不算有锁!
4.2产生死锁的四大必要条件:
1·互斥条件:
一个资源每次只能被一个执行流使用! 【锁本身的性质】
2·请求与保持条件:
一个执行流因请求资源而阻塞时,对已获得的资源保持不放 !【互相保持一个锁】
3·不剥夺条件:
一个执行流已获得的资源,在未使用完之前,不能强行剥夺!【他俩自己都不会unlock对方的锁】
4·循环等待条件:
若干执行流之间形成一种头尾相接的循环等待资源的关系 !【无对方锁就等着它释放】
因此总结成:有锁-->只有一把(访问资源需要两把)-->不去释放别人的锁-->自然就死等着!!
4.3如何打破死锁:
那么这些条件只要干掉一个就解开这个死锁问题了!
下面我们以破坏循环等待条件问题问题为例:资源一次性分配,使用超时机制、加锁顺序一致!
超时机制:用pthread mutex trylock;到时间自动解开那把锁!
资源一次性分配与加锁顺序一致:所有线程保证先有锁1然后锁2;下面我们采用c++代码演示一下:
思路:
锁一次性分配+加锁顺序一致;模拟十个线程进行双锁限定下访问共享资源
测试代码:
#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()
//模拟多线程同时访问共享资源的场景 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)
//等待所有线程完成 for (auto &thread : threads)
{
thread.join();
}
// 输出共享资源的最终状态 std::cout
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;
}
测试效果:
正常:
把双锁限制去掉;故就是线程不安全了:
每次结果都是不一样的;这里就不解释原因了-->原子性!!!
4.4避免死锁算法:
死锁检测算法(预防死锁):
参考大佬的博客:【项目展示】C语言实现死锁检测算法_计算机操作系统死锁的检测与解除算法源码c语言-CSDN博客
银行家算法(避免死锁;防止环路等待的出现):
参考大佬博客:
银行家算法课程设计(附源代码)_银行家算法代码-CSDN博客
五·STL与智能指针就线程安全分析:
5.1 STL中的容器是否是线程安全的?
不安全!!!STL的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的响;而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶);因此STL默认不是线程安全.如果需要在多线程环境下使用,往往需要调用者自行保证线程安全.!!!
【之前对他们加锁锁如blockqueue实现的时候的lock;这里就不解释了】
5.2 智能指针是否是线程安全的?
对于unique_ptr,由于只是在当前代码块范围内⽣效,因此不涉及线程安全问题:
对于shared_ptr,多个对象需要共⽤⼀个引⽤计数变量,所以会存在线程安全问题.但是标准库实现的时候考虑到了这个问题,基于原⼦操作(CAS)的⽅式保证shared_ptr能够⾼效,原⼦的操作引⽤计数:
【当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。】
后面我们会讲解CAS是如何操作的!!!
六·对其他锁分类的认识:
①悲观锁:
在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。我们常用的锁!
②乐观锁:
每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和上面的shared ptr使用的CAS操作。
③CAS操作:
当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
读图速解:
④自旋锁,读写锁:
自旋锁是忙等待锁,线程获锁失败时持续检查,适合短时间持锁场景,能避免线程切换开销,但久等会浪费 CPU 资源。读写锁区分读、写操作,允许多线程同时读,写操作需独占,适用于读多写少场景,可提升并发性能 。
这些都暂时了解下即可!!!
七·本篇查漏补缺:
7.1线程池优点:
1·降低资源消耗:通过重用已经创建的线程来降低线程创建和销毁的消耗!
2·.提高线程的可管理性:线程池可以统一管理、分配、调优和监控!
3·.程序性能更优:创建的线程越多性能越高!
4·.降低程序的耦合程度:提高程序的运行效率!
7.2何为临时资源:
消息,信号,信号量,中断等!
1·消息:
进程之间进行通信时传递的信息单元。例如,一个进程向另一个进程发送请求消息,接收方处理完后可能回送响应消息。这些消息在传递和处理过程中是临时存在的资源,处理完成后就不再需要保留。
2·中断:
硬件设备向CPU发送的信号,用于通知CPU发生了某个事件,如键盘输入、磁盘读写完成等。中断是临时产生的,CPU响应处理完后,该中断相关的资源(如中断信号、相关的寄存器状态等)就会被清理或重置。
3·信号量:
用于进程同步和互斥的一种机制。信号量的值会根据进程的操作而动态变化,它在进程间协调资源访问时临时发挥作用,当不再需要对相关资源进行协调控制时,信号量的作用就结束了。
7.3哪种操作需要cpu参与(也就是会被中断)
比如运算,比较(x++;x==y)等等这些就要转到cpu中有可能会被打断;但是比如a=1;这样的就会直接覆盖物理内存对应的值即可!
7.4线程池作用:
线程池实现基于:
通过一个线程安全的阻塞任务队列加上一个或一个以上的线程实现,线程池中的线程可以从阻塞队列中获取任务进行任务处理,当线程都处于繁忙状态时可以将任务加入阻塞队列中,等到其它的线程空闲后进行处理。
线程池实际作用:
可以避免大量线程频繁创建或销毁所带来的时间成本,也可以避免在峰值压力下,系统资沪耗尽的风险;并且可以统一对线程池中的线程进行管理,调度监控。
八·本篇小结:
本篇文章学习了日志,线程池(实现就没过多展开分析;代码注释也标注了相关细节处理),线程安全与重入,死锁等知识;以及查漏补缺做的总结;希望对大家学习这块有帮助! 此篇也是线程同步与互斥的最后一篇;过后对于线程这座大山;不敢说多么熟练;但是也是有了一定的个人的见解等;希望越来越努力的去学习之后的“大山”;并以博客的方式记录下(对于线程这块的学习;学习理解-->整理学习笔记-->查漏补缺-->总结博客(在复习一遍)也花费了很长很长的时间;只能说时间是挤出来的;愿挤总是有的;加油
!!!)