【Linux】系统部分——线程安全与线程的单例模式

30.2 线程安全与线程的单例模式

文章目录

在前面实现了线程池的基本结构之后,我们需要从实操再回到理论,谈一谈线程安全有关的话题

线程安全

  • 线程安全:

    线程安全是指在多个线程访问公共资源时,程序能正确执行且不会出现互相干扰或数据不一致的情况。当多个线程并发运行同一段代码时,只有局部变量会被访问,而不会出现访问全局资源或未保护的共享资源的情况。运行期间不会出现数据不一致、崩溃或其他异常结果。例如,抢票时票数变为负数的情况就是线程不安全的体现。此外,使用STL容器或多线程操作自定义内存空间时,若未正确处理共享资源,也会导致线程安全问题。线程安全是多线程编程中最常见的问题之一,类似于C语言中指针问题的普遍性。

  • 重入:

    • 同⼀个函数被不同的执⾏流调⽤,当前⼀个流程还没有执⾏完,就有其他的执⾏流再次进⼊,我们称之为重⼊

    • 可重入函数是指同一个函数被不同执行流(如多线程或信号处理)重复调用时,不会出现任何问题。若函数在重入过程中出现问题,则称为不可重入函数。重入场景主要分为两种:多线程重复调用函数和信号导致的重入。在多进程环境中,函数虽然可能被重复调用,但由于变量是独立拷贝的,通常不会出现线程安- 全问题。而在多线程或信号处理中,由于共享资源的存在,可能导致重入问题。

线程安全与可重入性是两个不同维度的概念。可重入性描述的是函数能否被多个执行流重复调用而不出问题,而线程安全描述的是线程在执行过程中是否会出现资源冲突或数据不一致。

  • 一般情况下:可重入函数是线程安全的,因为其定义允许被多个执行流安全调用。

  • 但线程安全的函数不一定是可重入的,例如,若函数通过加锁保护全局资源,虽然多线程调用安全,但单线程递归调用时可能因未释放锁而导致死锁,此时函数是线程安全但不可重入的。大多数函数是不可重入的,只有不访问任何全局资源的函数才可能是可重入的。线程安全问题通常是由于访问了不可重入函数或未保护的全局资源导致的

    举例说明:

    cpp 复制代码
    pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
    int global_counter = 0;
    
    // 一个线程安全的函数
    void increment_counter() {
        pthread_mutex_lock(&lock);
        global_counter++;
        pthread_mutex_unlock(&lock);
    }
    • 线程安全吗? 是安全的。多个线程同时调用它,global_counter 也能被正确递增。
    • 可重入吗? 不可重入
      • 想象一下,在单线程中:increment_counter() 被调用,它获得了锁。
      • 在它持有锁的期间,程序被一个信号中断,而信号处理函数也调用了 increment_counter()
      • 信号处理函数试图去获取同一把锁 lock,但这把锁已经被同一个线程持有了。
      • 这会导致死锁!该线程在等待自己释放锁,永远等不到。
常见的线程不安全的情况
  • 不保护共享变量的函数

  • 函数状态随着被调⽤,状态发⽣变化的函数

  • 返回指向静态变量指针的函数

  • 调⽤线程不安全函数的函数

常⻅的线程安全的情况
  • 每个线程对全局变量或者静态变量只有读取的权限,⽽没有写⼊的权限,⼀般来说这些线程是安全的
  • 类或者接⼝对于线程来说都是原⼦操作
  • 多个线程之间的切换不会导致该接⼝的执⾏结果存在⼆义性
常⻅不可重⼊的情况
  • 调⽤了malloc/free函数,因为malloc函数是⽤全局链表来管理堆的
  • 调⽤了标准I/O库函数,标准I/O库的很多实现都以不可重⼊的⽅式使⽤全局数据结构
  • 可重⼊函数体内使⽤了静态的数据结构
常⻅可重⼊的情况
  • 不使⽤全局变量或静态变量
  • 不使⽤malloc或者new开辟出的空间
  • 不调⽤不可重⼊函数
  • 不返回静态或全局数据,所有数据都有函数的调⽤者提供
  • 使⽤本地数据,或者通过制作全局数据的本地拷⻉来保护全局数据
死锁

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

线程安全的单例模式

​ 线程安全的单例模式是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点。

​ 线程池在使用时需要控制线程数量,根据CPU核心数合理设置线程数,通常为CPU核心数的两倍。线程池设计为单例模式便于统一管理和使用。单例模式分为饿汉式和懒汉式两种实现方式。饿汉式在类加载时就创建实例,懒汉式在首次使用时才创建实例。懒汉式通过延迟加载提高资源利用率,适用于体积较大的对象,可以加速程序启动时间。延迟加载的思想在Linux系统中广泛应用,如写时拷贝、缺页中断和内存地址空间申请等机制。

​ 懒汉⽅式最核⼼的思想是 "延时加载". 从⽽能够优化服务器的启动速度

懒汉方式实现单例模式
cpp 复制代码
template <typename T>
class Singleton
{
    static T *inst;

public:
    static T *GetInstance()
    {
        if (inst == NULL)
        {
            inst = new T();
        }
        return inst;
    }
};
饿汉方式实现单例模式
cpp 复制代码
template <typename T>
class Singleton {
	static T data;
public:
	static T* GetInstance() {
		return &data;
	}
};
将之前写的线程池改成单例模式(懒汉方式)

要点:

  • 在类中直接创建单例,我们要禁止使用拷贝构造,拷贝赋值,并把构造函数设置为私有,防止外部调用构造函数创建对象

    cpp 复制代码
    class ThreadPool
    {
    private:
       //....
       ThreadPool(const ThreadPool &) = delete;
       ThreadPool<T> &operator=(const ThreadPool &) = delete;
       ThreadPool(int num = defaultnum) : _num(num), _isrunning(false)
       {
           for (int i = 0; i < _num; i++)
           {
               _threads.push_back(std::make_shared<Thread>(std::bind(&ThreadPool::HanderTask, this, std::placeholders::_1)));
               LOG(LogLevel::INFO) << "创建线程: " << _threads.back()->Name() << "...Success";
           }
       }
       //.... 
        
    };
  • 在类中直接创建单例,这个单例要设置为静态成员变量,并在类外定义

    cpp 复制代码
    namespace My_ThreadPool
    {
        class ThreadPool
    	{
    	private:
        	//....
        	static ThreadPool<T> *instence;
        	//....
    	}
    
    	template <class T>
    	ThreadPool<T> *ThreadPool<T>::instence = NULL;
    }
  • 提供单例的静态成员函数

    cpp 复制代码
    public:
    //......
    static ThreadPool<T> *getInstence()
    {
        if (instence == NULL)
        {
            LOG(LogLevel::INFO) << "单例首次被执行,需要加载对象...";
            instence = new ThreadPool<T>();
        }
        return instence;
    }
    //.......
  • 单例的使用方法

    cpp 复制代码
    ThreadPool<task_t>::getInstence()->Start();
    for(int i = 0; i < 10; i++)
    {
        ThreadPool<task_t>::getInstence()->Equeue(Push);
        sleep(1);
    }
    
    ThreadPool<task_t>::getInstence()->Stop();
    
    ThreadPool<task_t>::getInstence()->Wait();
  • 如果有多个线程使用这个单例------调用getInstence函数,由于单例是全局的对象,所以需要加锁

    cpp 复制代码
    namespace My_ThreadPool
    {
        class ThreadPool
    	{
        public:
            static ThreadPool<T> *getInstence()
            {
                if (instance == NULL)//防止重复加锁
                {
                    LockGuard lockguard(mutex);
                    if (instence == NULL)
                    {
                        LOG(LogLevel::INFO) << "单例首次被执行,需要加载对象...";
                        instence = new ThreadPool<T>();
                    }
                }
                return instence;
            }
    	private:
        	//....
        	static Mutex mutex;
        	//....
    	}
    
    	template <typename T>
        Mutex ThreadPool<T>::mutex; // 只用来保护单例
    }

整个代码:

cpp 复制代码
#pragma once

#include <iostream>
#include <unistd.h>
#include <string>
#include <vector>
#include <queue>
#include <memory>
#include "Log.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
#include "Thread.hpp"

namespace My_ThreadPool
{
    using namespace My_Mutex;
    using namespace My_Log;
    using namespace My_Cond;
    using namespace My_Thread;

    using thread_t = std::shared_ptr<Thread>;

    const static int defaultnum = 5;

    void DefaultTest()
    {
        while (true)
        {
            LOG(LogLevel::DEBUG) << "Test......";

            sleep(1);
        }
    }

    template <class T>
    class ThreadPool
    {
    private:
        bool IsEmpty() { return _ptasks.empty(); }

        void HanderTask(std::string name)
        {
            T t;
            LOG(LogLevel::INFO) << "线程: " << name << "执行HanderTask方法";
            while (true)
            {
                {
                    LockGuard lockguard(_lock);
                    while (IsEmpty() && _isrunning)
                    {
                        _wait_num++;
                        _cond.Wait(_lock);
                        _wait_num--;
                    }

                    if (IsEmpty() && !_isrunning)
                        break;

                    t = _ptasks.front();
                    _ptasks.pop();
                }

                t(name);
            }
            LOG(LogLevel::INFO) << "线程: " << name << "退出HanderTask方法";
        }

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

        ThreadPool(int num = defaultnum) : _num(num), _isrunning(false)
        {
            for (int i = 0; i < _num; i++)
            {
                _threads.push_back(std::make_shared<Thread>(std::bind(&ThreadPool::HanderTask, this, std::placeholders::_1)));
                LOG(LogLevel::INFO) << "创建线程: " << _threads.back()->Name() << "...Success";
            }
        }

    public:
        static ThreadPool<T> *getinstance()
        {
            if (instance == NULL)
            {
                LockGuard lockguard(mutex);
                if (instance == NULL)
                {
                    LOG(LogLevel::INFO) << "单例首次被执行,需要加载对象...";
                    instance = new ThreadPool<T>();
                }
            }
            return instance;
        }

        void Equeue(T t)
        {
            LockGuard lockguard(_lock); // 加锁
            if (!_isrunning)
                return;
            _ptasks.push(t);
            if (_wait_num)
                _cond.Weak();
        }
        void Start()
        {
            if (_isrunning)
                return;
            _isrunning = true;
            for (auto &t : _threads)
            {
                t->Start();
                LOG(LogLevel::INFO) << "启动线程: " << t->Name() << "...Success";
            }
        }
        void Wait()
        {
            for (auto &t : _threads)
            {
                t->Jion();
                LOG(LogLevel::INFO) << "线程: " << t->Name() << "回收";
            }
        }
        void Stop()
        {
            if (_isrunning)
            {
                _isrunning = false;
                if (_wait_num)
                    _cond.WeakAll(); // 如果有线程在可变参数下等待,唤醒所有线程,把没有执行完成的任务(如果有)去执行
            }
        }
        ~ThreadPool() {}

    private:
        std::vector<thread_t> _threads; // 这里储存的是一个一个的指向Thread的智能指针
        int _num;                       // 线程个数
        std::queue<T> _ptasks;          // 任务的指针队列

        Mutex _lock;
        Cond _cond;
        int _wait_num;

        bool _isrunning; // 线程池目前的工作状态

        static ThreadPool<T> *instance;
        static Mutex mutex;
    };

    template <class T>
    ThreadPool<T> *ThreadPool<T>::instance = NULL;
    template <typename T>
    Mutex ThreadPool<T>::mutex; // 只用来保护单例
}
cpp 复制代码
#include "ThreadPool.hpp"
#include "Task.hpp"

using namespace My_ThreadPool;

int main()
{
    ENABLE_CONSOLE_LOG();
    
    ThreadPool<task_t>::getinstance()->Start();
    for(int i = 0; i < 10; i++)
    {
        ThreadPool<task_t>::getinstance()->Equeue(Push);
        sleep(1);
    }

    ThreadPool<task_t>::getinstance()->Stop();

    ThreadPool<task_t>::getinstance()->Wait();

    return 0;
}

执行结果

复制代码
user@iZ7xvdsb1wn2io90klvtwlZ:~/lesson34_3/SigThreadPool$ ./threadpool 
[2025-10-06 19:26:43] [INFO] [177014] [ThreadPool.hpp] [88] - 单例首次被执行,需要加载对象...
[2025-10-06 19:26:43] [INFO] [177014] [ThreadPool.hpp] [76] - 创建线程: thread-1...Success
[2025-10-06 19:26:43] [INFO] [177014] [ThreadPool.hpp] [76] - 创建线程: thread-2...Success
[2025-10-06 19:26:43] [INFO] [177014] [ThreadPool.hpp] [76] - 创建线程: thread-3...Success
[2025-10-06 19:26:43] [INFO] [177014] [ThreadPool.hpp] [76] - 创建线程: thread-4...Success
[2025-10-06 19:26:43] [INFO] [177014] [ThreadPool.hpp] [76] - 创建线程: thread-5...Success
[2025-10-06 19:26:43] [INFO] [177014] [ThreadPool.hpp] [112] - 启动线程: thread-1...Success
[2025-10-06 19:26:43] [INFO] [177014] [ThreadPool.hpp] [112] - 启动线程: thread-2...Success
[2025-10-06 19:26:43] [INFO] [177014] [ThreadPool.hpp] [112] - 启动线程: thread-3...Success
[2025-10-06 19:26:43] [INFO] [177014] [ThreadPool.hpp] [112] - 启动线程: thread-4...Success
[2025-10-06 19:26:43] [INFO] [177014] [ThreadPool.hpp] [112] - 启动线程: thread-5...Success
[2025-10-06 19:26:43] [INFO] [177014] [ThreadPool.hpp] [44] - 线程: thread-4执行HanderTask方法
[2025-10-06 19:26:43] [INFO] [177014] [ThreadPool.hpp] [44] - 线程: thread-1执行HanderTask方法
[2025-10-06 19:26:43] [DEBUG] [177014] [Task.hpp] [19] - 我是一个推送数据到服务器的一个任务, 我正在被执行[thread-4]
[2025-10-06 19:26:43] [INFO] [177014] [ThreadPool.hpp] [44] - 线程: thread-2执行HanderTask方法
[2025-10-06 19:26:43] [INFO] [177014] [ThreadPool.hpp] [44] - 线程: thread-5执行HanderTask方法
[2025-10-06 19:26:43] [INFO] [177014] [ThreadPool.hpp] [44] - 线程: thread-3执行HanderTask方法
[2025-10-06 19:26:44] [DEBUG] [177014] [Task.hpp] [19] - 我是一个推送数据到服务器的一个任务, 我正在被执行[thread-1]
[2025-10-06 19:26:45] [DEBUG] [177014] [Task.hpp] [19] - 我是一个推送数据到服务器的一个任务, 我正在被执行[thread-4]
[2025-10-06 19:26:46] [DEBUG] [177014] [Task.hpp] [19] - 我是一个推送数据到服务器的一个任务, 我正在被执行[thread-2]
[2025-10-06 19:26:47] [DEBUG] [177014] [Task.hpp] [19] - 我是一个推送数据到服务器的一个任务, 我正在被执行[thread-5]
[2025-10-06 19:26:48] [DEBUG] [177014] [Task.hpp] [19] - 我是一个推送数据到服务器的一个任务, 我正在被执行[thread-3]
[2025-10-06 19:26:49] [DEBUG] [177014] [Task.hpp] [19] - 我是一个推送数据到服务器的一个任务, 我正在被执行[thread-1]
[2025-10-06 19:26:50] [DEBUG] [177014] [Task.hpp] [19] - 我是一个推送数据到服务器的一个任务, 我正在被执行[thread-4]
[2025-10-06 19:26:51] [DEBUG] [177014] [Task.hpp] [19] - 我是一个推送数据到服务器的一个任务, 我正在被执行[thread-2]
[2025-10-06 19:26:52] [DEBUG] [177014] [Task.hpp] [19] - 我是一个推送数据到服务器的一个任务, 我正在被执行[thread-5]
[2025-10-06 19:26:53] [INFO] [177014] [ThreadPool.hpp] [65] - 线程: thread-3退出HanderTask方法
[2025-10-06 19:26:53] [INFO] [177014] [ThreadPool.hpp] [65] - 线程: thread-2退出HanderTask方法
[2025-10-06 19:26:53] [INFO] [177014] [ThreadPool.hpp] [65] - 线程: thread-5退出HanderTask方法
[2025-10-06 19:26:53] [INFO] [177014] [ThreadPool.hpp] [65] - 线程: thread-1退出HanderTask方法
[2025-10-06 19:26:53] [INFO] [177014] [ThreadPool.hpp] [65] - 线程: thread-4退出HanderTask方法
[2025-10-06 19:26:53] [INFO] [177014] [ThreadPool.hpp] [120] - 线程: thread-1回收
[2025-10-06 19:26:53] [INFO] [177014] [ThreadPool.hpp] [120] - 线程: thread-2回收
[2025-10-06 19:26:53] [INFO] [177014] [ThreadPool.hpp] [120] - 线程: thread-3回收
[2025-10-06 19:26:53] [INFO] [177014] [ThreadPool.hpp] [120] - 线程: thread-4回收
[2025-10-06 19:26:53] [INFO] [177014] [ThreadPool.hpp] [120] - 线程: thread-5回收
相关推荐
开压路机6 小时前
进程控制
linux·服务器
香蕉鼠片6 小时前
跨平台开发到底是什么
linux·windows·macos
bukeyiwanshui8 小时前
20260417 DNS实验
linux
代码中介商9 小时前
Linux 帮助手册与用户管理完全指南
linux·运维·服务器
bluechips·zhao9 小时前
帝国CMS 8.0 安全审计分析——代码审计
安全·网络安全·代码审计
whuhewei9 小时前
为什么客户端不存在跨域问题
前端·安全
Flittly9 小时前
【SpringSecurity新手村系列】(4)验证码功能实现
java·spring boot·安全·spring
Flittly9 小时前
【SpringSecurity新手村系列】(3)自定义登录页与表单认证
java·笔记·安全·spring·springboot
TechWayfarer10 小时前
攻防对抗:利用IP段归属查询工具快速封禁攻击源——3步联动防火墙(附脚本)
python·网络协议·tcp/ip·安全
weixin_4491736511 小时前
Linux -- 项目中查找日志的常用Linux命令
linux·运维·服务器