Linux--线程安全、死锁、单例线程池问题

诡异森林。 2024-07-26 10:37:02 阅读 63

线程系列:

Linux–线程的认识(一)

Linux–线程的分离、线程库的地址关系的理解、线程的简单封装(二)

线程的互斥:临界资源只能在同一时间被一个线程使用

生产消费模型

信号量

线程池(包含日志的解释)

线程安全

线程安全指的是在多线程环境下,某个操作或方法被多个线程并发执行时,能够保持数据的一致性和完整性,即不会因为多个线程的交替执行而导致数据被破坏或程序的行为不符合预期

简单来说,如果一个操作在多线程环境下执行时,能够保证数据的一致性和操作的原子性,那么这个操作就是线程安全的

关键因素

原子性:操作或方法在执行过程中,要么全部完成,要么完全不执行,不会被其他线程的操作打断。可见性:一个线程对共享变量的修改,能够立即被其他线程看到。有序性:程序执行的顺序按照代码的先后顺序执行,不会因为重排序而导致数据错乱。

问题来源

共享资源:多个线程访问同一个资源(如变量、文件等),如果没有适当的同步机制,就可能导致数据不一致。竞态条件:两个或多个线程同时执行,且它们的执行顺序和结果依赖于它们的相对执行速度,可能导致程序行为不可预测。

死锁

死锁是指两个或多个进程(线程)在执行过程中,因竞争资源或由于彼此通信而造成的一种阻塞现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程。

产生原因

竞争不可抢占资源:当系统中供多个进程共享的资源(如打印机、公用队列等)数量不足以满足诸进程的需要时,会引起进程对资源的竞争而产生死锁。竞争可消耗资源:除了不可抢占资源外,对可消耗资源(如内存)的争夺也可能引起死锁。进程推进顺序不当:进程在运行过程中,如果推进顺序不当,也可能导致死锁的发生。

产生的必要条件

互斥条件:至少有一个资源被标记为独占,即一次只能被一个进程(线程)使用。请求与保持条件:进程(线程)已经持有了至少一个资源,并且在等待获取其他进程(线程)持有的资源。不可剥夺条件:已经分配给进程(线程)的资源不能被强制性地剥夺,只能由进程(线程)自己释放。循环等待条件:存在一个进程(线程)的资源请求链,使得每个进程(线程)都在等待下一个进程(线程)所持有的资源。

预防方法

破坏互斥条件:对于某些资源,可以允许多个进程(线程)同时访问,而不是独占资源。例如,使用读写锁代替互斥锁。破坏请求与保持条件:在申请新资源之前,释放已经持有的资源。只有在获得所有需要的资源后,才重新申请资源。破坏不可剥夺条件:允许操作系统强制性地剥夺某些进程(线程)所持有的资源,并将其分配给其他等待资源的进程(线程)。破坏循环等待条件:对资源进行全局排序,并按照顺序申请资源,避免形成循环等待链。例如,可以使用银行家算法来避免死锁。

解除死锁的方法

资源剥夺法:挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。撤销进程法:强制撤销部分或全部死锁进程并剥夺这些进程的资源,直至打破循环环路,使系统从死锁状态中解脱出来。撤销的原则可以按进程优先级和撤销进程代价的高低进行。进程回退法:让一个或多个进程回退到足以避免死锁的地步,进程回退时自愿释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。

银行家算法

在计算机系统中,对于每种资源都设置一个最大需求量和当前可用量。当一个进程提出资源请求时,系统首先检查该进程是否满足其最大需求量限制,如果请求合法,则尝试分配资源给该进程并检查是否会导致系统进入不安全状态(即可能发生死锁),如果不会,则分配资源;否则,拒绝该进程的请求。

算法步骤

1.检查请求是否合法

如果Request[i] <= Need[i](请求的资源数不超过进程i还需要的资源数)且Request[i] <= Available(请求的资源数不超过系统中现有的可用资源数),则请求合法,否则请求不合法,拒绝分配。

2.模拟分配资源

假设为进程i分配资源,更新相关数据结构:Available = Available - Request[i]Allocation[i] = Allocation[i] + Request[i]Need[i] = Need[i] - Request[i]

3.执行安全性算法

安全性算法用于检查此次资源分配后,系统是否处于安全状态。具体步骤如下:

设置两个向量:Work(表示系统当前可提供给进程继续执行所需要的各类资源的数目,初值为Available)和Finish(表示系统是否有足够的资源分配给进程,使之完成运行,初值全为false)。从进程集合中寻找满足Finish[i] = false且Need[i] <= Work的进程,若找到,则执行以下操作:

– 进程i获得资源,可顺利执行,完成释放所分配的资源:Work = Work + Allocation[i],Finish[i] = true。重复上述步骤,直到所有进程的Finish都为true(表示系统处于安全状态),或无法再找到满足条件的进程(表示系统处于不安全状态)。

4.根据检查结果决定是否真正分配资源

如果安全性算法结果为系统处于安全状态,则真正进行资源分配;如果系统处于不安全状态,则撤销此次模拟分配,恢复原来状态,拒绝申请。

单例问题

在上一篇中,我们使用的线程池是可以无限制的创建的,如果每个任务或请求都创建一个新的线程池,那么当任务或请求量很大时,会导致创建大量的线程池实例,每个实例内部又可能包含多个线程,这将极大地浪费系统资源,包括内存和CPU时间。

使用单例模式可以确保整个应用或系统只存在一个线程池实例,这样所有的任务或请求都可以共享这个线程池中的线程,避免了资源的浪费;

单例的线程池

<code>#pragma once

#include<iostream>

#include<vector>

#include<queue>

#include<pthread.h>

#include"Thread.hpp"

#include"Log.hpp"

#include"LockGuard.hpp"

using namespace ThreadMdule;

using namespace std;

const static int gdefaultthreadnum=3;//默认线程池的线程数

template <class T>

class ThreadPool

{

public:

ThreadPool(int threadnum=gdefaultthreadnum) :_threadnum(threadnum),_waitnum(0),_isrunning(false)

{

pthread_mutex_init(&_mutex,nullptr);

pthread_cond_init(&_cond,nullptr);

LOG(INFO,"ThreadPool COnstruct.");

}

//禁用拷贝赋值

ThreadPool(const ThreadPool<T>&) = delete;

ThreadPool<T>& operator=(const ThreadPool<T>&) = delete;

//单例:懒汉模式

static ThreadPool<T>* GetInstance()

{

//再次判断,减少获取单例的加锁成本,保证线程安全

if(_instance==nullptr)

{

LockGuard lockguard(&_lock);

if(_instance==nullptr)

{

_instance= new ThreadPool<T>();

_instance->InitThreadPool();

_instance->Start();

LOG(DEBUG,"创建单例线程池");

return _instance;

}

}

LOG(DEBUG,"获取单例线程池");

return _instance;

}

//各个线程独立的任务函数

void HandlerTask(string name)

{

LOG(INFO,"%s is running...",name.c_str());

while(true)

{

LockQueue();//开启保护

//等到有任务时才退出循环执行下列语句

while(_task_queue.empty()&&_isrunning)

{

_waitnum++;

ThreadSleep();

_waitnum--;

}

//当任务队列空并且线程池停止时线程退出

if(_task_queue.empty()&&!_isrunning)

{

UnlockQueue();

cout<<name<<" quit "<<endl;

sleep(1);

break;

}

//1.任务队列不为空&&线程池开启

//2.任务队列不为空&&线程池关闭,直到任务队列为空

//所以,只要有任务,就要处理任务

T t=_task_queue.front();//取出对应任务

_task_queue.pop();

UnlockQueue();

LOG(DEBUG,"%s get a task",name.c_str());

//处理任务

t();

LOG(DEBUG,"%s handler a task,result is: %s",name.c_str(),t.ResultToString().c_str());

}

}

//线程池中线程的构建

void InitThreadPool()

{

for(int i=0;i<_threadnum;i++)

{

string name="thread-"+to_string(i+1);code>

_threads.emplace_back(bind(&ThreadPool::HandlerTask,this,placeholders::_1),name);

LOG(INFO,"init thread %s done",name.c_str());

}

_isrunning=true;

}

//线程池的启动

void Start()

{

for(auto& thread:_threads)

{

thread.start();

}

}

//线程池停止

void Stop()

{

LockQueue();

_isrunning=false;

ThreadWakeupAll();

UnlockQueue();

}

void Wait()

{

for(auto& thread:_threads)

{

thread.Join();

LOG(INFO,"%s is quit...",thread.name().c_str());

}

}

//将任务入队列

bool Enqueue(const T& t)

{

bool ret=false;

LockQueue();

if(_isrunning)

{

_task_queue.push(t);

//如果有空闲的线程,那么唤醒线程让其执行任务

if(_waitnum>0)

{

ThreadWakeup();

}

LOG(DEBUG,"enqueue task success");

ret=true;

}

UnlockQueue();

return ret;

}

~ThreadPool()

{

pthread_mutex_destroy(&_mutex);

pthread_cond_destroy(&_cond);

}

private:

void LockQueue()

{

pthread_mutex_lock(&_mutex);

}

void UnlockQueue()

{

pthread_mutex_unlock(&_mutex);

}

void ThreadSleep()

{

pthread_cond_wait(&_cond, &_mutex);

}

void ThreadWakeup()

{

pthread_cond_signal(&_cond);

}

void ThreadWakeupAll()

{

pthread_cond_broadcast(&_cond);

}

int _threadnum;//线程数

vector<Thread> _threads;//存储线程的vector

queue<T> _task_queue;//输入的任务队列

pthread_mutex_t _mutex;//互斥锁

pthread_cond_t _cond;//条件变量

int _waitnum;//空闲的线程数

bool _isrunning;//表示线程池是否启动

//单例模式

static ThreadPool<T>* _instance;

static pthread_mutex_t _lock;

};

template<typename T>

ThreadPool<T>* ThreadPool<T>::_instance=nullptr;

template<typename T>

pthread_mutex_t ThreadPool<T>::_lock = PTHREAD_MUTEX_INITIALIZER;

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。