【Linux】线程池

目录

  • 一、线程池
    • [1.1 搭建线程池的初步框架](#1.1 搭建线程池的初步框架)
    • [1.2 初步粗糙实现线程池功能](#1.2 初步粗糙实现线程池功能)
    • [1.3 改动线程的执行函数](#1.3 改动线程的执行函数)
    • [1.4 初步补充线程的执行函数和入队列函数](#1.4 初步补充线程的执行函数和入队列函数)
    • [1.5 改动线程池停止接口](#1.5 改动线程池停止接口)
  • 二、单例模式线程池
    • [2.1 单例模式进程池实现](#2.1 单例模式进程池实现)
    • [2.2 单例模式线程池的线程安全问题](#2.2 单例模式线程池的线程安全问题)
  • 三、线程安全和重入问题
  • 四、常见锁概念
    • [4.1 死锁](#4.1 死锁)
    • [4.2 死锁的四个必要条件](#4.2 死锁的四个必要条件)
    • [4.3 避免死锁](#4.3 避免死锁)
    • [4.4 避免死锁算法](#4.4 避免死锁算法)
    • 五、关于STL、智能指针的线程安全
    • [5.1 STL中的容器是否线程安全](#5.1 STL中的容器是否线程安全)
    • [5.2 智能指针是否线程安全](#5.2 智能指针是否线程安全)

个人主页:矢望

个人专栏:C++Linux系统编程C语言数据结构Coze-AIMySQL

一、线程池

Linux线程池是一种基于生产者-消费者模式的多线程管理技术,核心思想是预先创建一批固定数量的工作线程,并将它们置于阻塞等待状态,避免频繁的线程创建和销毁开销。当有新任务提交时,线程池通过任务队列接收任务,并利用互斥锁与条件变量同步机制唤醒一个空闲线程来执行;任务完成后该线程并不退出,而是重新回到等待状态 。这种设计能有效控制并发线程数量、降低系统资源消耗,并提高响应速度,广泛用于高并发服务器、批量数据处理等需要大量异步任务的场景。

如上,这样一眼望去很明显的能够看出来,这不就是我们之前博客说的生产者消费者模型嘛!

主线程的任务就是产生任务,选择线程,让线程处理任务

1.1 搭建线程池的初步框架

接下来,计划写一个线程池类,这个类将来通过Start、Stop等接口实现线程池的启停。成员变量中需要有变量能够标识线程池现在的状态(运行、停止等),还需要有容器组织各个线程,需要有一个队列存放任务等。

由于将来线程执行的任务的类型并不确定,所以存放任务的容器必须带有模板参数,因此我们的线程池类也必须是模板类

ThreadPool.hpp

cpp 复制代码
#include <iostream>
#include <vector>
#include <queue>
#include "Thread.hpp"
#include "Cond.hpp"
#include "Mutex.hpp"
#include "Logger.hpp"

namespace NS_THREAD_POOL_MODULE
{
    using namespace NS_THREAD_MODULE; // 使用封装的线程模块
    using namespace NS_LOG_MODULE; // 使用日志模块

    const int default_num = 5;

    template<typename T>
    class ThreadPool
    {
    public:
        ThreadPool(int slaver_num = default_num)
            : _slaver_num(slaver_num)
            , _isrunning(false)
            , _slaver_sleep_num(0)
        {}

        void Start() // 线程池启动
        {}

        void Stop() // 线程池停止
        {}

        void Wait() // 等待线程
        {}

        void Enqueue(T in) // 任务加入接口
        {}

        ~ThreadPool()
        {}
    private:
        bool _isrunning; // 标识线程池是否正在运行
        int _slaver_num; // 标识线程的数量
        int _slaver_sleep_num; // 标识处于等待状态的线程数量
        std::vector<Thread> _slavers; // 组织线程
        std::queue<T> _tasks; // 任务队列
        Cond _cond; // 条件变量
        Mutex _mutex; // 互斥锁
    };
}

如上,线程池类的框架基本完成,上面的线程模块,日志模块,互斥锁,条件变量等等,之前全部封装过,都在前面的博客中有体现。

1.2 初步粗糙实现线程池功能

构造函数中应该将需要的线程数量全部创建出来,创建多个线程类出来。

cpp 复制代码
void Test()
{
    while(true)
    {
        char name[1024];
        pthread_getname_np(pthread_self(), name, sizeof name); // 获取在线程类中设置的线程名
        LOG(LogLevel::DEBUG) << "线程运行测试,线程名:" << name;
        sleep(1);
    }
}

template<typename T>
class ThreadPool
{
public:
    ThreadPool(int slaver_num = default_num)
        : _slaver_num(slaver_num)
        , _isrunning(false)
        , _slaver_sleep_num(0)
    {
        for(int idx = 1; idx <= slaver_num; idx++)
        {
            _slavers.emplace_back(Test); // 增加线程类
        }
    }
    //...
};

将来线程被唤醒之后就会去执行Test方法。

Start、Stop、Wait接口:

cpp 复制代码
void Start() // 线程池启动
{
    if(_isrunning) 
    {
        LOG(LogLevel::WARNING) << "ThreadPool Is Already Running!";
        return;
    }

    _isrunning = true;
    for(auto &slaver :_slavers)
    {
        slaver.Start(); // 在线程类内部创建并启动线程
    }
}

void Stop() // 线程池停止
{
    if(!_isrunning)
    {
        LOG(LogLevel::WARNING) << "ThreadPool Is Not Running!";
        return;
    }

    _isrunning = false;
    for(auto &slaver : _slavers)
    {
        slaver.Die();
    }
}

void Wait() // 等待线程
{
    for(auto &slaver : _slavers)
    {
        slaver.Join();
    }
}

Main.cc测试:

cpp 复制代码
#include <memory>
#include "ThreadPool.hpp"
#include "Logger.hpp"

using namespace NS_THREAD_POOL_MODULE; // 线程池模块
using namespace NS_LOG_MODULE; // 日志模块

int main()
{
    SET_CONSOLE_LOG_STRATEGY(); // 使用日志的控制台模式
    std::unique_ptr<ThreadPool<int>> tp= std::make_unique<ThreadPool<int>>();

    tp->Start();
    sleep(5);

    tp->Stop();
    tp->Wait();

    sleep(3);

    return 0;
}

如上,在进行运行测试的时候,我们应该看到一瞬间创建出5个线程然后它们各自开始执行,五秒后只剩下了一个主线程,然后三秒后程序终止。

运行结果:

如上,线程池的运行符合预期。

1.3 改动线程的执行函数

今天的代码,我不希望创建的线程去执行类外的函数,我想让线程执行类内的函数。

cpp 复制代码
template<typename T>
class ThreadPool
{
private:
    void HandlerTask()
    {
        while(true)
        {
            char name[1024];
            pthread_getname_np(pthread_self(), name, sizeof name); // 获取在线程类中设置的线程名
            LOG(LogLevel::DEBUG) << "线程运行测试,线程名:" << name;
            sleep(1);
        }
    }
public:
    ThreadPool(int slaver_num = default_num)
        : _slaver_num(slaver_num)
        , _isrunning(false)
        , _slaver_sleep_num(0)
    {
        for(int idx = 1; idx <= slaver_num; idx++)
        {
            _slavers.emplace_back(HandlerTask); // 增加线程类
        }
    }
    // ...
};

当然,如果将函数写在类内,那么传递的时候就会报错,因为线程类接收的是返回值为void,参数为空的函数类型。而类内的非静态成员函数默认有一个this指针参数,所以参数类型对不上,就会报错

解决这个问题有如下方法,方法一就是将这个成员函数定义成静态成员函数,这样就没有this指针了,但是定义为静态成员函数就会导致它访问不了类内的成员变量,这不是我们想要的。所以这种方法不行

方法二就是使用Lambda表达式捕获this指针

cpp 复制代码
ThreadPool(int slaver_num = default_num)
 : _slaver_num(slaver_num)
    , _isrunning(false)
    , _slaver_sleep_num(0)
{
    for(int idx = 1; idx <= slaver_num; idx++)
    {
        // _slavers.emplace_back(Test); // 增加线程类
        // 使用 lambda 表达式捕获 this
        _slavers.emplace_back([this](){
            this->HandlerTask();
        });
    }
}

在编译就可以编译通过了:

方法三就是使用bind进行参数绑定

cpp 复制代码
ThreadPool(int slaver_num = default_num)
   : _slaver_num(slaver_num)
    , _isrunning(false)
    , _slaver_sleep_num(0)
{
    for(int idx = 1; idx <= slaver_num; idx++)
    {
        // _slavers.emplace_back(Test); // 增加线程类

        // // 使用 lambda 表达式捕获 this
        // _slavers.emplace_back([this](){
        //     this->HandlerTask();
        // });
        
        // 使用 std::bind 绑定 this 和成员函数
        _slavers.emplace_back(std::bind(&ThreadPool::HandlerTask, this));
    }
}

这样也可以编译通过:

最终采取Lambda表达式捕获this指针的方式。

流程图:

1.4 初步补充线程的执行函数和入队列函数

cpp 复制代码
void HandlerTask()
{
    while(true)
    {
        char name[1024];
        pthread_getname_np(pthread_self(), name, sizeof name); // 获取在线程类中设置的线程名
        
        T task;
        // 临界区
        {
            LockGuard lockguard(_mutex);
            // 检测任务
            while(_tasks.empty()) // 使用 while 循环,避免虚假唤醒等问题
            {
                // 没有任务,休眠
                _slaver_sleep_num++;
                _cond.Wait(_mutex);
                _slaver_sleep_num--;
            }
            
            // 有任务,取任务
            task = _tasks.front();
            _tasks.pop();
        }

        // 处理任务
        LOG(LogLevel::INFO) << name << "线程,处理任务:";
        task();
    }
}

如上是线程的执行函数,分为如下几步:检测任务,取任务,处理任务。其中检测任务和取任务需要在临界区下完成,而在取任务之后,本质任务就从公共变成了线程的私有。所以处理任务是可以在临界区外完成的。

接下来,再来完善生产任务的接口:

cpp 复制代码
void Enqueue(T in) // 任务加入接口
{
    // 临界区
    {
        LockGuard lockguard(_mutex);
        _tasks.push(in);
        if(_slaver_sleep_num > 0) // 存在休眠线程,唤醒
            _cond.Signal();
    }
}

如上,虽然只有一个主线程加入任务,但是同一个队列只能有一个线程进行操作(存/取),所以必须加锁。

测试代码:

cpp 复制代码
using task_t = std::function<void ()>;

int main()
{
    SET_CONSOLE_LOG_STRATEGY(); // 使用日志的控制台模式
    std::unique_ptr<ThreadPool<task_t>> tp= std::make_unique<ThreadPool<task_t>>();

    tp->Start();

    for(int i = 1; i <= 10; i++)
    {
        tp->Enqueue([](){
            LOG(LogLevel::DEBUG) << "任务正在被处理...";
        });
        sleep(1);
    }

    tp->Stop();
    tp->Wait();

    return 0;
}

编译运行:

1.5 改动线程池停止接口

现在线程池停止接口很明显是不合理的,调用Stop接口之后,无论线程是否执行完了派发给它的任务,线程都会直接被强制停止。

应该是让线程执行完队列中的所有任务再停止,所以应该有一个将线程全部广播唤醒完成任务的操作

cpp 复制代码
void Stop() // 线程池停止
{
    if(!_isrunning) // 避免重复停止
    {
        LOG(LogLevel::WARNING) << "ThreadPool Is Not Running!";
        return;
    }

    {
        LockGuard lockguard(_mutex);
        _isrunning = false;
        if(_slaver_sleep_num > 0)
            _cond.BroadCast(); // 将线程全部唤醒
    }            
}

调用Stop接口并且线程处理完所有任务之后就需要退出了,所以HandlerTask函数中的死循环需要有退出循环的逻辑,这样线程就会自然退出。设计成死循环是因为一个线程并不是只可能处理一个任务。

cpp 复制代码
void HandlerTask()
{
	char name[1024];
    pthread_getname_np(pthread_self(), name, sizeof name); // 获取在线程类中设置的线程名
    while(true)
    {
        T task;
        // 临界区
        {
            LockGuard lockguard(_mutex);
            // 检测任务, 进程池不停止 并且 任务队列为空 才休眠
            while(_tasks.empty() && _isrunning) // 使用 while 循环,避免虚假唤醒等问题
            {
                // 没有任务,休眠
                _slaver_sleep_num++;
                _cond.Wait(_mutex);
                _slaver_sleep_num--;
            }
            
            if(_tasks.empty() && !_isrunning)
                break; // 互斥锁会自动解锁,不需要在 break 前手动解锁

            // 有任务, 取任务, 本质:把任务由公共变成私有
            task = _tasks.front();
            _tasks.pop();
        }

        // 处理任务
        LOG(LogLevel::INFO) << name << "线程,处理任务:";
        task();
    }

    // 线程退出
    LOG(LogLevel::INFO) << name << "线程退出...";
}

测试Main.cc

cpp 复制代码
using task_t = std::function<void ()>;

int main()
{
    SET_CONSOLE_LOG_STRATEGY(); // 使用日志的控制台模式
    std::unique_ptr<ThreadPool<task_t>> tp= std::make_unique<ThreadPool<task_t>>();

    tp->Start();

    for(int i = 1; i <= 10; i++)
    {
        tp->Enqueue([](){
            LOG(LogLevel::DEBUG) << "任务正在被处理...";
        });
    }

    tp->Stop();
    LOG(LogLevel::INFO) << "进程池已停止运行,线程立刻处理完所有任务!";
    tp->Wait();

    return 0;
}

如上,加入任务的速度很快,线程来不及处理完,进程池就停止了。我们应该看到线程在退出之前将队列中的所有任务处理完成。

编译运行:

如上图,线程在处理完成队列中的所有任务之后才退出的,符合预期。

二、单例模式线程池

单例模式就是只允许在加载或者运行期间,整体最多创建一个该类对象。单例模式存在的根本原因,是为了在程序全局强制共享"唯一的一份资源" ,从而解决两个痛点:一是节省资源 ,像日志系统、数据库连接池、配置管理器这类重资源对象,如果每个模块都 new 一个,内存和文件句柄会被迅速耗尽;二是保证状态一致 ,如果有多份实例,各自持有独立状态(比如不同的配置版本),程序行为就会混乱不可控。通过单例,你从"大家各自 copy"变成"所有人共用同一份",既避免了重复开销,也确保了全局数据同步。

单例模式确保一个类只有一个实例,并提供全局访问点,在 C++/Linux 中常用于管理日志、配置或连接池等资源。核心实现要点是:私有化构造函数禁止外部创建,静态公有方法返回唯一实例,并处理多线程安全(C++11 起可用局部静态变量实现线程安全的懒加载,无需加锁),同时注意禁止拷贝和赋值,防止内存泄漏

也就是首先要把特定类的构造函数,拷贝构造,赋值语句等全部私有化,拷贝构造,赋值语句甚至可以删除

创建对象的时机有如下两种,一种是加载到内存的时候,创建对象 ,例如int val = 100;另一种是进程在运行期间创建对象 ,例如int *val = (int*)malloc(sizeof(int));。在实际的实践当中最长用的是第二种方式。

在单例的实现中存在饿汉实现方式和懒汉实现方式,举个例子,饿汉实现方式就是 吃完饭,立刻洗碗,这种就是饿汉方式。 因为下一顿吃的时候立刻拿着碗就能吃饭。懒汉实现方式就是 吃完饭,先把碗放下,然后下一顿饭用到这个碗了再洗碗。饿汉实现方式对应的就是加载到内存时,创建对象;懒汉实现方式对应的就是在进程运行期间创建对象

饿汉实现单例模式示例:

cpp 复制代码
template <typename T>
class Singleton 
{
	static T data;
public:
	static T* GetInstance() 
	{
		return &data;
	}
};

懒汉实现单例模式示例:

cpp 复制代码
template <typename T>
class Singleton 
{
	static T* inst;
public:
	static T* GetInstance() 
	{
		if (inst == NULL) 
		{
			inst = new T();
		}
		return inst;
	}
};

饿汉实现方式实现单例在加载到内存的时候就需要创建对象,而懒汉实现方式实现单例是在使用的时候才创建对象,它的核心思想就是延时加载。当代码的模块非常大的时候,如果采用饿汉的话启动就需要花费很长时间,但懒汉模式就可以优化启动速度了

2.1 单例模式进程池实现

接下来就使用懒汉实现方式修改进程池代码。首先就是构造函数私有化,并且禁止拷贝构造和赋值语句

cpp 复制代码
template<typename T>
class ThreadPool
{
private:
    // ...

    ThreadPool(int slaver_num = default_num)
        : _slaver_num(slaver_num)
        , _isrunning(false)
        , _slaver_sleep_num(0)
    {
        for(int idx = 1; idx <= slaver_num; idx++)
        {
            // _slavers.emplace_back(Test); // 增加线程类

            // 使用 lambda 表达式捕获 this
            _slavers.emplace_back([this](){
                this->HandlerTask();
            });
            
            // 使用 std::bind 绑定 this 和成员函数
            // _slavers.emplace_back(std::bind(&ThreadPool::HandlerTask, this));
        }
    }

    // 禁止 拷贝构造 赋值语句
    ThreadPool(const ThreadPool<T> &) = delete;
    ThreadPool<T>& operator=(const ThreadPool<T>&) = delete;
    
    // ...
};

声明静态的单例成员,并在类外进行定义初始化

cpp 复制代码
template<typename T>
class ThreadPool
{
private:
    // ...

    // 添加单例模式
    static ThreadPool<T> *_instance; // 声明
};

// 定义 + 初始化
template<typename T>
ThreadPool<T> *ThreadPool<T>::_instance = nullptr;

添加单例入口函数Instance

cpp 复制代码
template<typename T>
class ThreadPool
{
private:
    // ...
public:
    static ThreadPool<T>* Instance()
    {
        if(_instance == nullptr) 
        {
            // 第一次调用
            _instance = new ThreadPool<T>();
            _instance->Start(); // 启动线程池
            LOG(LogLevel::INFO) << "第一次使用线程池,创建单例对象成功!";
        }
        return _instance; // 不是第一次调用就直接返回单例对象
    }
};

这个入口函数将来直接可以ThreadPool<T>::Instance->的方式使用,如果这个函数不是静态的话就必须现有对象才可以调用Instance函数,但是单例模式就是为了保证只能创建一个对象,这就变成了蛋生鸡鸡生蛋问题,所以入口函数需要设计成静态函数

测试Main.cc

cpp 复制代码
using task_t = std::function<void ()>;

int main()
{
    SET_CONSOLE_LOG_STRATEGY(); // 使用日志的控制台模式

    for(int i = 1; i <= 5; i++)
    {
        ThreadPool<task_t>::Instance()->Enqueue([](){
            LOG(LogLevel::DEBUG) << "任务正在被处理...";
        });
    }

    ThreadPool<task_t>::Instance()->Stop();
    ThreadPool<task_t>::Instance()->Wait();

    return 0;
}

运行结果:

2.2 单例模式线程池的线程安全问题

单例模式线程池作为一个模块,可能会有多个线程进行访问,此时单例的入口函数Instance就是线程不安全的

cpp 复制代码
static ThreadPool<T>* Instance()
{
    if(_instance == nullptr) 
    {
        // 第一次调用
        _instance = new ThreadPool<T>();
        _instance->Start(); // 启动线程池
        LOG(LogLevel::INFO) << "第一次使用线程池,创建单例对象成功!";
    }
    return _instance; // 不是第一次调用就直接返回单例对象
}

如上,可能多个线程都判断if条件成立,然后它们都进入了语句内部,这样就导致了创建出了多个对象,很明显这是不允许的。

所以就需要一把互斥锁来保护单例线程池。这把互斥锁也必须是静态的,因为静态函数只能访问静态成员

静态互斥锁:

cpp 复制代码
private:
    // ...

    // 添加单例模式
    static ThreadPool<T> *_instance; // 声明
    static Mutex _lock; // 保证单例的线程安全
};

// 定义 + 初始化
template<typename T>
ThreadPool<T> *ThreadPool<T>::_instance = nullptr;

template<typename T>
Mutex ThreadPool<T>::_lock;

保证入口函数的线程安全:

cpp 复制代码
static ThreadPool<T>* Instance()
{
    {
        LockGuard lockguard(_lock); // 保证单例入口函数的线程安全

        if(_instance == nullptr) 
        {
            // 第一次调用
            _instance = new ThreadPool<T>();
            _instance->Start(); // 启动线程池
            LOG(LogLevel::INFO) << "第一次使用线程池,创建单例对象成功!";
        }
    }
    return _instance; // 不是第一次调用就直接返回单例对象
}

如上,这样的确保证了线程安全,但是效率比较低,单例线程池对象创建出来之后,再有线程进入,它还是会加锁,判断if条件,解锁,返回。这样效率太低了。

我们可以再加一个if条件,如下:

cpp 复制代码
static ThreadPool<T>* Instance()
 {
     if(_instance == nullptr) // 双 if 判断,保证效率
     {
         LockGuard lockguard(_lock); // 保证单例入口函数的线程安全

         if(_instance == nullptr) 
         {
             // 第一次调用
             _instance = new ThreadPool<T>();
             _instance->Start(); // 启动线程池
             LOG(LogLevel::INFO) << "第一次使用线程池,创建单例对象成功!";
         }
     }
     return _instance; // 不是第一次调用就直接返回单例对象
 }

如上,在单例线程池对象还没有创建出来的时候,确实会有很多的线程同时进入第一个if语句内部,但是互斥锁依旧保证了线程安全,当线程串行访问并且单例线程池创建出来之后,再有线程进入函数,第一个if判断直接不满足,它就不需要再加锁解锁了,直接就返回了,这样就保证了效率

三、线程安全和重入问题

线程安全 :就是多个线程在访问共享资源时,能够正确地执行,不会相互干扰或破坏彼此的执行结果。一般而⾔,多个线程并发同一段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进行操作,并且没有锁保护的情况下,容易出现该问题。

重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

常见的线程安全的情况

1、每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。

2、类或者接口对于线程来说都是原子操作。

3、多个线程之间的切换不会导致该接口的执行结果存在二义性。

常见的线程不安全的情况

不保护共享变量的函数

1、函数状态随着被调用,状态发生变化的函数。

2、返回指向静态变量指针的函数。

3、调用线程不安全函数的函数。

常见可重入的情况

1、不使用全局变量或静态变量。

2、不使用 malloc或者new开辟出的空间。

3、不调用不可重入函数。

4、不返回静态或全局数据,所有数据都有函数的调用者提供。

5、使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。

常见不可重入的情况

1、调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的。

2、调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

3、可重入函数体内使用了静态的数据结构。

线程安全侧重点在线程并发执行上;重入与不可重入是函数的特点。它们两者是概念上的不同类别

它们两个在操作层面上有交集,这是必然的,毕竟线程会调用函数

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

可重入比线程安全更严格:线程安全可以依赖锁,可重入连锁都不能用,只能靠局部变量

四、常见锁概念

4.1 死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态

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

这样造成的结果就是:

4.2 死锁的四个必要条件

1、互斥条件 :一个资源每次只能被一个执行流使用。

2、请求与保持条件 :一个执行流因请求资源而阻塞时,对已获得的资源保持不放。

3、不剥夺条件 :一个执行流已获得的资源,在末使用完之前,不能强行剥夺。

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

4.3 避免死锁

避免死锁的方法就是破坏死锁的四个必要条件。

1、破坏互斥:让资源可共享。

2、破坏请求与保持:一次性申请所有资源(要么全拿,要么全不拿)。

3、破坏不剥夺:允许强行剥夺资源。

4、破坏循环等待:给资源编号,所有线程按固定顺序申请。

4.4 避免死锁算法

死锁检测算法、银行家算法。

死锁检测算法 :在系统运行中定期检查资源分配图,若能找到一组无法推进的进程集合则判定发生死锁,然后进行恢复;允许死锁发生但能及时发现。

银行家算法:在每次资源分配前模拟分配并检查系统是否仍处于安全状态(即存在安全序列让所有进程完成),若不安全则拒绝本次分配;它通过预先声明最大资源需求来预防死锁,但运行开销较大。

五、关于STL、智能指针的线程安全

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

不安全,STL 的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。

而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶)。

因此 STL 默认不是线程安全的。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全。

5.2 智能指针是否线程安全

智能指针是线程安全的,但智能指针指向的对象不一定线程安全。

对于 unique_ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题。

对于 shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证 shared_ptr 能够高效,原子的操作引用计数。

总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~

相关推荐
FREEDOM_X4 小时前
Linux 多线程编程——总结
java·linux·运维·ubuntu
筱羽_筱羽5 小时前
跟着“Ubuntu18.04/20.04编译Linux5.10.76+Xenomai-3.2.1+IgH”教程遇到的问题
linux·运维·ubuntu
ShineWinsu7 小时前
对于Linux:基于UDP实现简单聊天室功能
linux·c++·面试·udp·笔试·进程·简单聊天室
zoipuus7 小时前
上传repo仓库到自己的gitlab
linux·gitlab·shell·全志·tina
IT大白鼠7 小时前
CVE-2026-7531 Azure Linux MariaDB PQC 内存破坏漏洞详解
linux·mariadb·azure·cve-2026-7531
无忧.芙桃7 小时前
线程同步与线程互斥(上)
linux·运维·操作系统·运维开发
HAPPY酷7 小时前
【ROS2】16G 内存笔记本跑 ROS2 仿真?VMware 虚拟机“保姆级”配置指南 (R9 7940HX + RTX 4060)
java·linux·ide·windows·pycharm
donoot17 小时前
Linux系统下图书馆级电子书全自动标准化分类整理完整实施方案
大数据·linux·运维·电子书管理
Felix-lxd18 小时前
Ubuntu 22.04 配置 Nginx
linux·nginx·ubuntu