【Linux】多线程 —— 线程池 | 单例模式 | 常见锁

🌈欢迎来到Linux专栏 ~~ 线程池

线程池

线程池(ThreadPool)是一种基于池化思想管理和使用线程的机制。它是将多个线程预先存储在一个池子内,当有任务出现时可以避免重新创建和销毁线程所带来性能开销,只需要从"池子"内取出相应的线程执行对应的任务即可。

!在这里插入图片描述(https://i-blog.csdnimg.cn/direct/daf7c82944bd4a9280c7ceedc10ba18f.pn# 一、线程池的概念

🐸池化技术

所谓的 线程池 就是 提前创建一批线程,当任务来临时,线程直接从任务队列中获取任务执行,这种设计模式可以显著提高系统整体效率。与传统的"来一个任务创建一个线程"的方式相比,线程池避免了频繁创建和销毁线程带来的性能开销。

像这种把未来会高频使用到,并且创建较为麻烦的资源提前申请好的技术称为 池化技术 。池化技术的核心思想是资源复用,通过预先分配和管理资源,减少资源创建和销毁的开销,从而提高系统性能。

池化技术 可以极大地提高性能,最典型的就是 线程池,常用于各种涉及网络连接相关的服务中,比如:

  • MySQL 连接池:数据库连接是昂贵的资源,连接池可以复用已建立的连接,避免频繁建立和断开连接
  • HTTP 连接池:在Web服务中,复用HTTP连接可以减少TCP三次握手的时间开销
  • Redis 连接池:缓存服务中,连接池可以快速响应数据读写请求
  • 对象池:在游戏开发中,频繁创建和销毁游戏对象时使用
  • 内存池:操作系统和编程语言运行时中管理内存分配

除了线程池外还有内存池,比如 STL 中的容器在进行空间申请时,都是直接从 空间配置器 allocator 中获取的,并非直接使用系统调用来申请空间。内存池通过预先分配一大块内存,然后按需分配给程序使用,减少了频繁调用 malloc/freenew/delete 的系统开销。

池化技术 的本质:空间换时间。通过预先占用一定的存储空间(资源池),来换取运行时的时间效率提升。

池化技术 就好比你把钱从银行提前取出一部分放在支付宝中,可以随时使用,十分方便和高效,总不至于需要用钱时还得跑到银行排队取钱。同样地,线程池就像是一个"线程银行",需要时直接取用,用完归还,避免了每次都要"开户销户"的繁琐过程。

🐸线程池的优点

线程池 的优点在于 高效、方便

  • 线程在使用前就已经创建好了,使用时直接将任务交给线程完成
  • 线程会被合理调度,确保 任务与线程 间能做到负载均衡

线程池 中的线程数量不是越多越好,因为线程增多会导致调度变复杂,具体创建多少线程取决于具体业务场景,比如 处理器内核、剩余内存、网络中的 socket 数量等

线程池 还可以配合 「生产者消费者模型」 一起使用,做到 解耦与提高效率

  • 可以把 任务队列 换成 「生产者消费者模型」

🐸线程池的应用场景

线程池 有以下几种应用场景:

  • 存在大量且短小的任务请求 ,比如 Web 服务器中的网页请求,使用 线程池 就非常合适,因为网页点击量众多,并且大多都没有长时间连接访问
  • 对性能要求苛刻,力求快速响应需求,比如游戏服务器,要求对玩家的操作做出快速响应
  • 突发大量请求,但不至于使服务器产生过多的线程,短时间内,在服务器创建大量线程会使得内存达到极限,造成出错,可以使用 线程池 规避问题

二、线程池的实现

由于我们之前封装过锁、条件队列、日志以及线程等,所以我们此处不再调用系统调用去实现这个线程池,而是通过上述封装好去进行实现

♦️大致框架

创建 ThreadPool.hpp 头文件

线程池 实现为一个类,提供接口供外部调用

首先要明白 线程池 的两大核心:一批线程 与 任务队列,客户端发出请求,新增任务,线程获取任务,执行任务,因此大体框架如下

  • 一批线程,通过容器管理
  • 任务队列,存储就绪的任务
  • 互斥锁
  • 条件变量

互斥锁 的作用是 保证多个线程并访问任务队列时的线程安全 ,而 条件变量 可以在 任务队列 为空时,让一批线程进入等待状态,也就是线程同步

在实现之前, 我们先捋顺整个代码的逻辑关系

这就是下层软件调用上层方法 ,设置 + 回调

大致框架如下:

cpp 复制代码
#pragma once

#include <iostream>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include "Logger.hpp"
#include "Thread.hpp"

namespace NS_THREAD_POOL
{
    using namespace NS_Thread_Module;
    using namespace NS_LOG_MODULE;

    const int defaultnum = 5;

    template <typename T> //模版化任务
    class ThreadPool
    {
    private:
        void HandlerTask()
        {
        }

    public:
        ThreadPool(int slaver_num = defaultnum)
            : _isRunning(true), _slaver_sleep_count(0), _slaver_num(slaver_num)
        {
        }

        void Stop()
        {
        }

        void Enqueue(T in)
        {
        }

        ~ThreadPool()
        {
        }

    private:
        bool _isRunning;
        int _slaver_num;
        std::vector<Thread> _slavers;
        std::queue<T> _tasks;
        std::mutex _mutex;
        std::condition_variable _cond;
        int _slaver_sleep_count;
    };
}

接下来是逐个填补函数体

构造线程池 ThreadPool()

cpp 复制代码
ThreadPool(int slaver_num = defaultnum)
    : _isRunning(true), _slaver_sleep_count(0), _slaver_num(slaver_num)
{
    for (int idx = 0; idx < _slaver_num; ++idx)
    {
        _slavers.emplace_back([this](){
            this->HandlerTask();
        });
    }
}

线程池构建 :对于每个线程都emplace_back构造线程,回调执行HandlerTask()函数

构建线程这里设计为不需要传参!因为使用了匿名函数,直接绑定的就是类内的成员函数,后续回调回来执行的也是HandlerTask()

启动线程池 start() --- 位于 ThreadPool 类

cpp 复制代码
for (auto &slaver : _slavers)
{
    slaver.Start();
}

创建一批线程后,紧接着让每个线程都启动即可

线程池停止 Stop()

cpp 复制代码
void Stop()
{
	if(!_isrunning)
	{
		//日志打印 :该线程池已经不在跑了
		return;
	}
	for(auto &slave : _slavers)
	{
		slave.Die(); //简单粗暴
	}
	_isrunning = false;
}

此处是直接简单粗暴的让全部线程都去Die了, 后续对这个函数再进行优化,还有细节没考虑到

重头戏HandlerTask() 处理任务函数

所有的线程启动后,都会回调来执行这个任务函数

cpp 复制代码
void HandlerTask()
{
    char name[128];
    pthread_getname_np(pthread_self(), name, sizeof(name));
    while (true)
    {
        _mutex.lock();
        //检测任务队列s是否为空,如果为空就睡觉,等待唤醒
        while (_tasks.empty() && _isRunning) //防止伪唤醒 while
        {
            //没有任务
            _slaver_sleep_count++;
            _cond.wait(_mutex);
            _slaver_sleep_count--;
        }
        //有任务:在队列里取出任务
        T task = _tasks.front();
        _tasks.pop();
        _mutex.unlock();
        //处理任务?
        LOG(LogLevel::INFO) << name << "正在处理任务: ";
        task();
    }
}

此处的大体逻辑:加锁------ 检测任务(对临界资源 -- 队列) ------ 没有任务 ------ 有任务(取出任务) ------ 执行任务

细节如下:

  • 为了防止条件变量伪唤醒 ,把 if 改成 while

  • 取出任务的本质:把任务由公共变成私有 ,该任务只能由我一个线程知道了,所以处理任务也不会影响其他人 ------ 没有其他线程来抢了,那么还需要用锁保护吗??

  • 处理任务是要在锁内进行吗?不需要的,如果在锁内进行处理,本质上所有任务都是串行执行了,那怎么体现出线程池的高效呢??

Enqueue(T in) 往队列里加任务

cpp 复制代码
void Enqueue(T in)
{
    _mutex.lock();
    _tasks.push(in);
    if (_slaver_sleep_count > 0) //只要有一个线程在睡觉,就唤醒它
    {
        _cond.Broadcast();
    }
    _mutex.unlock();
}

创建 main.cc 源文件,测试线程池的代码

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

#include <memory>
#include <ctime>
#include <iostream>
#include <functional>
#include <cstdlib>

using namespace NS_THREAD_POOL;
using namespace NS_LOG_MODULE;

// using task_t = std::function<void()>; // task 是一个 std::function 对象:包装了一个无参无返回值的可调用对象

class Task
{
public:
    Task(){}
    Task(int x, int y) : _x(x), _y(y)
    {}
    void operator()()
    {
        _result = _x + _y;
    }   
    std::string Result()
    {
        return std::to_string(_x) + " + " + std::to_string(_y) + " = " + std::to_string(_result);
    }
    ~Task(){}
private:
    int _x;
    int _y;
    int _result;
};

int main()
{
    ENABLE_CONSOLE_LOG_STRATEGY();
    srand((long)time(nullptr) ^ getpid());

    ThreadPool<Task> tp;
    int cnt = 10;
    while (cnt--)
    {
        int x = rand() % 10 + 1;
        usleep(11111);
        int y = rand() % 20;
        Task t(x, y);
        tp.Enqueue(t);
        sleep(1);
    }
    return 0;
}

至此整个线程池的大致框架已经搭出来了

♦️整体逻辑

整体逻辑我们来理顺一下:

  1. main()启动

    cpp 复制代码
    Main.cc:37  main()
      ├─ ENABLE_CONSOLE_LOG_STRATEGY()    → 设置日志策略 ------ 向显示器打印
      ├─ srand(...)                       → 初始化随机种子
      └─ ThreadPool<Task> tp;             → 构造 ThreadPool(栈对象)
  2. ThreadPool 构造

    cpp 复制代码
    ThreadPool::ThreadPool()
      ├─ _isRunning = true, _slaver_sleep_count = 0
      ├─ 创建 5 个 Thread 对象,每个绑定 lambda: [this](){ HandlerTask(); }
      │   (此时线程还未真正创建,只是 C++ 对象构造)
      └─ 对每个 Thread 调用 slaver.Start()
           └─ Thread::Start() → pthread_create(&_tid, nullptr, ThreadRoutine, this)
  3. pthread_create → 内核创建线程 → 执行 ThreadRoutine

    cpp 复制代码
    ThreadRoutine(void* args)
      ├─ self = static_cast<Thread*>(args)
      ├─ pthread_setname_np(self->_tid, self->_name)
      ├─ self->_cb()             → 即 HandlerTask()
      ├─ self->ToStop()          → _statue = THREAD_STOP
      └─ return nullptr          → 线程退出
  4. 接着执行HandlerTask() --- 工作线程主循环

    cpp 复制代码
    while (true) {
        _mutex.Lock();
        while (_tasks.empty() && _isRunning) {   // 无任务且池子在运行
            _slaver_sleep_count++;               // 记录休眠线程数
            _cond.Wait(_mutex);                  // 阻塞等待条件变量 → 解锁+挂起
            _slaver_sleep_count--;               // 被唤醒
        }
        if (!_isRunning && _tasks.empty()) {     // 池子停止且无任务
            _mutex.Unlock();
            break;                               // → 退出循环
        }
        // 取出任务
        task = _tasks.front(); _tasks.pop();
        _mutex.Unlock();
        task();                                  // 执行 Task::operator()
        // 继续 while(true)
    }
    LOG(DEBUG) << name << "退出了线程池";        // 线程退出前打印

    这里初始时队列为空 ,且构造时 _isRunning=true,所以 5 个线程都会进入 _cond.Wait() 进入阻塞睡眠状态。

  5. main 循环入队

    cpp 复制代码
    while (cnt--) {             // 10 次
        Task t(x, y);
        tp.Enqueue(t);          → LockGuard → _tasks.push(t)
                                  → if _slaver_sleep_count > 0 → _cond.Broadcast()
                                  → LockGuard 析构解锁
        sleep(1);
    }

    每次 Enqueue 调用 Broadcast 唤醒所有等待线程 。被唤醒的线程从 _cond.Wait 返回(已持锁),检查 while 条件发现队列非空退出内层 while取出任务 → 解锁 → 执行 task()(打印计算结果)→ 继续下一轮循环 → 又可能因队列空而再次 Wait。

  6. main 结束tp 析构

    cpp 复制代码
    ~ThreadPool()
      ├─ if (_isRunning) Stop();
      │     ├─ LockGuard → _isRunning = false
      │     ├─ if _slaver_sleep_count > 0 → _cond.Broadcast()
      │     └─ LockGuard 析构解锁
      └─ for (auto &slaver : _slavers) slaver.Join();
            └─ pthread_join(_tid, &_result)

    stop的时候也会把所有的线程全部唤醒(以防万一)

  7. 线程被 Broadcast 唤醒后退出

    cpp 复制代码
    从 _cond.Wait 返回(持锁)
    while (_tasks.empty() && _isRunning)  → _isRunning=false,不进入
    if (!_isRunning && _tasks.empty())    → true
        _mutex.Unlock(); break;
    // 退出 while(true)
    LOG(DEBUG) << "退出了线程池";          // 打印
    // HandlerTask 返回 → _cb() 返回
    → ThreadRoutine 恢复执行:
      ├─ self->ToStop()
      └─ return nullptr  → 线程终止
  8. 主线程 pthread_join 逐个回收

    cpp 复制代码
    for 每个线程 → pthread_join 阻塞等待对应线程退出
    → 所有线程回收完毕
    → ThreadPool 析构完成
    → tp 对象销毁
    → main() return 0 → 进程退出

♦️优化

1️⃣退出逻辑 需要优化:

不能简单粗暴地直接让线程die了,因为有些线程可能还在执行任务,会导致当前的任务并没有处理完

  • _isrunning = false ------ 线程状态设置为false
  • 处理完成tasks所有的任务 。此时线程的状态有很多种:休眠、正在处理任务;让所有线程全部唤醒
  • 让线程处理完任务后正常结束:HandlerTask自动break
cpp 复制代码
void Stop()
{
    {
        LockGuard lock(_mutex);
        _isRunning = false;
        if (_slaver_sleep_count > 0)
            _cond.Broadcast();
    }
}

那么HandlerTask如何自动break呢? ------ 什么情况下才会去休眠呢?

  • 线程池还在跑 && 队列为空

那么如果线程池退出了就可以break了吗?❌️不一定,任务队列里任务要处理完

  • 所以退出必须满足:线程池退出了 && 任务队列为空
cpp 复制代码
void HandlerTask()
{
    char name[128];
    pthread_getname_np(pthread_self(), name, sizeof(name));
    while (true)
    {
        _mutex.Lock();
        while (_tasks.empty() && _isRunning)
        {
            _slaver_sleep_count++;
            _cond.Wait(_mutex);
            _slaver_sleep_count--;
        }
        //线程池退出了 && 任务队列为空才进行 解锁------ break
        if (!_isRunning && _tasks.empty())
        {
            _mutex.Unlock();
            break;
        }
        T task = _tasks.front();
        _tasks.pop();
        _mutex.Unlock();
        LOG(LogLevel::INFO) << name << "正在处理任务: ";
        task();
    }
}

2️⃣RAII风格加锁

加锁全部换成:LockGuard lock(_mutex);

源码如下:

cpp 复制代码
#pragma once

#include <iostream>
#include <vector>
#include <queue>
#include "Logger.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include "Mutex.hpp"

namespace NS_THREAD_POOL
{
    using namespace NS_Thread_Module;
    using namespace NS_LOG_MODULE;

    const int defaultnum = 5;

    template <typename T> // 模版化任务
    class ThreadPool
    {
    private:
        void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while (true)
            {
                T task;
                {
                    // 保护临界区
                    LockGuard lock(&_mutex);
                    // 初始五个线程都在此进行休眠等
                    while (_tasks.empty() && _isRunning)
                    {
                        _slaver_sleep_count++;
                        _cond.Wait(_mutex);
                        _slaver_sleep_count--;
                    }
                    if (!_isRunning && _tasks.empty())
                    {
                        _mutex.Unlock();
                        break;
                    }
                    task = _tasks.front();
                    _tasks.pop();
                }
                LOG(LogLevel::INFO) << name << "正在处理任务: ";
                task();
                LOG(LogLevel::DEBUG) << task.Result();
            }
            // 线程退出
            LOG(LogLevel::DEBUG) << name << "退出了线程池";
        }

    public:
        ThreadPool(int slaver_num = defaultnum)
            : _isRunning(true), _slaver_sleep_count(0), _slaver_num(slaver_num)
        {
            for (int idx = 0; idx < _slaver_num; ++idx)
            {
                _slavers.emplace_back([this]()
                                      { this->HandlerTask(); });
            }
            for (auto &slaver : _slavers)
            {
                slaver.Start();
            }
        }

        void Stop()
        {
            {
                LockGuard lock(&_mutex);
                _isRunning = false;
                if (_slaver_sleep_count > 0)
                    _cond.Broadcast();
            }
        }

        void Enqueue(T in)
        {
            LockGuard lock(&_mutex);
            _tasks.push(in);
            if (_slaver_sleep_count > 0)
            {
                _cond.Broadcast();
            }
        }

        ~ThreadPool()
        {
            if (_isRunning)
            {
                Stop();
            }
            for (auto &slaver : _slavers)
            {
                slaver.Join();
            }
        }

    private:
        bool _isRunning;
        int _slaver_num;
        std::vector<Thread> _slavers;
        std::queue<T> _tasks;
        Mutex _mutex;
        Cond _cond;
        int _slaver_sleep_count;
    };
}

三、单例模式

🌳什么是单例模式

在一个程序中只允许实例化出一个对象 ,可以通过 单例模式 来实现,单例模式 是非常 经典、常用、常考 的设计模式

什么是设计模式?

设计模式就是计算机大佬们在长时间项目实战中总结出来的解决方案,是帮助菜鸡编写高质量代码的利器,常见的设计模式有 单例模式、建造者模式、工厂模式、代理模式等

🌳单例模式的特点

某些类,只应该具有⼀个对象(实例) ,就称之为单例(例如⼀个男人只能有⼀个媳妇)

在很多服务器开发场景中,经常需要让服务器加载很多的数据(上百G)到内存中。此时往往要用一个单例的类来管理这些数据

🌳单例模式的简单实现

创建对象的时机:

  1. 加载到内存的时候,创建对象 ------ int gval = 100
  2. 进程在运行期间,创建对象 ------ int *val = (int*)malloc(sizeof(int))最佳实践🏆

单例模式就是只允许在加载或者运行期间,整体最多创建一个该类对象

单例模式 有两种实现方向:饿汉 与 懒汉

  • 首先把特定的类的构造函数,拷贝构造,赋值语句,全部私有化private,并且拷贝构造、赋值语句甚至可以删除了

比如看看下面这个类 Signal

cpp 复制代码
#pragma once

#include <iostream>

namespace Yohifo
{
    class Signal
    {
    private:
        // 构造函数私有化
        Signal()
        {}
        // 删除拷贝构造
        Signal(const Signal&) = delete;
    };
}

还有创建一个单例对象 没实现,既然外部受权限约束无法创建对象,那么类内是肯定可以创建对象的 ,只需要创建一个指向该类对象的 静态指针 或者一个 静态对象 ,再初始化就好了;因为外部无法访问该指针,所以还需要提供一个静态函数 getInstance() 以获取单例对象句柄,至于具体怎么实现,需要分不同方向(饿汉 or 懒汉)

cpp 复制代码
#pragma once

#include <iostream>

namespace Yohifo
{
    class Signal
    {
    private:
        // 构造函数私有化
        Signal()
        {}

        // 删除拷贝构造
        Signal(const Signal&) = delete;
     
     public:
     	// 获取单例对象的句柄 ------ 必须为静态
        static Signal *getInstance()
        {
            return _sigptr;
        }

        void print()
        {
            std::cout << "Hello Signal!" << std::endl;
        }
        
    private:
        // 指向单例对象的静态指针
        static Signal *_sigptr;
    };
}

此处为什么指向单例对象的是一个静态指针呢?

  • static 之后,整个程序只有一份 _instance,所有代码看的都是同一个指针
  • 通过静态函数获取_instance 从而通过地址调用该对象的其他函数 ------ (可以直接 ThreadPool<T>::Instance() 调用,不需要先有对象
  • getInstance()获取单例接口本身也应该声明为 static,否则外部在没有对象的情况下根本调不了它,那就成死循环了(鸡生蛋,蛋生鸡问题:我们没有对象就调用不了,但是我们getInstance就是为了获取对象的)

下面介绍饿汉实现方式和懒汉实现方式:以洗碗为例子

  • 吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.
  • 吃完饭, 先把碗放下, 然后下⼀顿饭用到这个碗了再洗碗, 就是懒汉方式

懒汉方式最核心的思想是"延时加载"。从而能够优化服务器的启动速度

😈恶汉模式

饿汉模式 也是如此,在程序加载到内存时 ,就已经早早的把 单例对象 创建好了(此时程序服务还没有完全启动),也就是在外部直接通过 new 实例化一个对象,具体实现如下

cpp 复制代码
#pragma once

#include <iostream>

namespace Yohifo
{
    // 饿汉模式
    class Signal
    {
    private:
        // 构造函数私有化
        Signal()
        {}

        // 删除拷贝构造
        Signal(const Signal&) = delete;

    public:
        static Signal *getInstance()
        {
            return _sigptr;
        }

        void print()
        {
            std::cout << "Hello Signal!" << std::endl;
        }

    private:
        // 指向单例对象的静态指针
        static Signal *_sigptr;
    };

    Signal* Signal::_sigptr = new Signal();
}

注:在程序加载时,该对象会被创建

这里的 单例对象 本质就有点像 全局变量,在程序加载时就已经创建好了

外部可以直接通过 getInstance() 获取 单例对象 的操作句柄,来调用类中的其他函数

Main.cc

cpp 复制代码
#include <iostream>
#include "Signal.hpp"

int main()
{
    //通过 getInstance() 获取单例对象的操作句柄,来调用类中的其他函数
    Ton::Signal::getInstance()->print();

    return 0;
}

运行结果为:

饿汉模式 是一个相对简单的单例实现方向,只需要在类中声明,在类外初始化就行了,但它也会带来一定的弊端:延缓服务启动速度

完全启动服务是需要时间的,创建单例对象 也是需要时间的,饿汉模式 在服务正式启动前会先创建对象,但凡这个单例类很大,服务启动时间势必会受到影响,大型项目启动,时间就是金钱

综上所述,饿汉模式 不是很推荐使用 ,除非图实现简单,并且服务规模较小;既然 饿汉模式 有缺点,就需要改进,于是就出现了 懒汉模式

🥸懒汉模式

懒汉模式 中,单例对象 并不会在程序加载时创建,而是在第一次调用时创建,第一次调用创建后,后续无需再创建,直接使用即可

cpp 复制代码
#pragma once

#include <iostream>

namespace Yohifo
{
    // 懒汉模式
    class Signal
    {
    private:
        // 构造函数私有化
        Signal()
        {}

        // 删除拷贝构造
        Signal(const Signal&) = delete;

    public:
        static Signal *getInstance()
        {
            // 第一次调用才创建
            if(_sigptr == nullptr)
            {
                _sigptr = new Signal();
            }

            return _sigptr;
        }

        void print()
        {
            std::cout << "Hello Signal!" << std::endl;
        }

    private:
        // 静态指针
        static Signal *_sigptr;
    };

    // 初始化静态指针
    Signal* Signal::_sigptr = nullptr;
}

注意 : 此时的静态指针需要初始化为 nullptr,方便第一次判断

饿汉模式 中出现的问题这里全都避免了

  • 创建耗时 -> 只在第一次使用时创建
  • 占用资源 -> 如果不使用,就不会被创建

懒汉模式 的核心在于 延时加载,可以优化服务器的速度及资源占用

延时加载这种机制就有点像 「写时拷贝」 ,就du你不会使用,从而节省资源开销,类似的还有 动态库、进程地址空间

这样看来,懒汉模式 确实优秀,实现起来也不麻烦,为什么会说 饿汉模式 更简单呢?

这是因为当前只是单线程 场景,程序暂时没啥问题,如果当前是多线程场景 ,问题就大了,如果一批线程同时调用 getInstance()同时认定 _sigptr 为空,就会创建多个 单例对象,这是不合理的

如何证明?

简单改一下代码,每创建一个单例对象,就打印一条语句,将代码放入多线程环境中测试

获取单例对象句柄 getInstance() --- 位于 Signal

cpp 复制代码
static Signal *getInstance()
{
    // 第一次调用才创建
    if(_sigptr == nullptr)
    {
        std::cout << "创建了一个单例对象" << std::endl;
        _sigptr = new Signal();
    }

    return _sigptr;
}

Main.cc其中使用了 lambda 表达式来作为线程的回调函数,重点在于查看现象

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include "Signal.hpp"

int main()
{
    // 创建一批线程
    pthread_t arr[10];
    for(int i = 0; i < 10; i++)
    {
        pthread_create(arr + i, nullptr, [](void*)->void*
            {
                // 获取句柄
                auto ptr = Yohifo::Signal::getInstance();
                ptr->print();
                return nullptr;
            }, nullptr);
    }

    for(int i = 0; i < 10; i++)
        pthread_join(arr[i], nullptr);

    return 0;
}

可以看见,当前代码在多线程环境中,同时创建了多个 单例对象 ,因此是存在线程安全问题的

饿汉模式没有线程安全问题吗?

没有,因为饿汉模式下,单例对象一开始就被创建了,即便是多线程场景中,也不会创建多个对象,它们也做不到

🥸懒汉模式(线程安全版)

有问题就解决,解决多线程并发访问的利器是 互斥锁 ,那就创建 互斥锁 保护单例对象的创建

cpp 复制代码
namespace Ton
{
    // 懒汉模式
    class Signal
    {
    private:
        // 构造函数私有化
        Signal()
        {
        }

        // 删除拷贝构造
        Signal(const Signal &) = delete;

    public:
        static Signal *getInstance()
        {
            // 加锁保护
            pthread_mutex_lock(&_lock);
            if (_sigptr == nullptr)
            {
                std::cout << "创建了一个单例对象" << std::endl;
                _sigptr = new Signal();
            }
            pthread_mutex_unlock(&_lock);
            return _sigptr;
        }

        void print()
        {
            std::cout << "Hello Signal!" << std::endl;
        }

    private:
        // 静态指针
        static Signal *_sigptr;
        static pthread_mutex_t _lock; // 保证单例的安全
    };

    // 初始化静态指针
    Signal *Signal::_sigptr = nullptr;
    // 初始化互斥锁
    pthread_mutex_t Signal::_lock = PTHREAD_MUTEX_INITIALIZER;
}

注意: getInstance() 是静态函数,互斥锁也要定义为静态的,可以初始化为全局静态锁

虽然此时已经能够满足线程安全,但此时又带来了:效率问题

  • 代码中只会有一次去创建一个单例对象,后续不会创建单例对象,也需要进行 加锁、判断、解锁 这个流程 ,要知道 加锁 也是有资源消耗的,所以这种写法不妥
  • 我需要的是只需第一次创建时才是加锁(串型),后续都不进行加锁------直接返回

解决方案是:DoubleCheck 双检查加锁

加锁 前再增加一层判断

cpp 复制代码
static Signal *getInstance()
{
    // 双if判断
    if(_sigptr == nullptr)
    {
        // 加锁保护
        pthread_mutex_lock(&_mtx);
        if(_sigptr == nullptr)
        {
            std::cout << "创建了一个单例对象" << std::endl;
            _sigptr = new Signal();
        }
        pthread_mutex_unlock(&_mtx);
    }
    
    return _sigptr;
}

这种写法实在是太优雅了 ------ 值得学习

🌳单例式线程池(最终版)

SignalTon.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <vector>
#include <queue>
#include "Logger.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include "Mutex.hpp"

namespace NS_THREAD_POOL
{
    using namespace NS_Thread_Module;
    using namespace NS_LOG_MODULE;

    const int defaultnum = 5;

    template <typename T> // 模版化任务
    class ThreadPool
    {
    private:
        void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while (true)
            {
                T task;
                {
                    // 保护临界区
                    LockGuard lock(&_mutex);
                    // 初始五个线程都在此进行休眠等
                    while (_tasks.empty() && _isRunning)
                    {
                        _slaver_sleep_count++;
                        _cond.Wait(_mutex);
                        _slaver_sleep_count--;
                    }
                    if (!_isRunning && _tasks.empty())
                    {
                        _mutex.Unlock();
                        break;
                    }
                    task = _tasks.front();
                    _tasks.pop();
                }
                LOG(LogLevel::INFO) << name << "正在处理任务: ";
                task();
                LOG(LogLevel::DEBUG) << task.Result();
            }
            // 线程退出
            LOG(LogLevel::DEBUG) << name << "退出了线程池";
        }
        // 构造放在私有里
        ThreadPool(int slaver_num = defaultnum)
            : _isRunning(true), _slaver_sleep_count(0), _slaver_num(slaver_num)
        {
            for (int idx = 0; idx < _slaver_num; ++idx)
            {
                _slavers.emplace_back([this]()
                                      { this->HandlerTask(); });
            }
            for (auto &slaver : _slavers)
            {
                slaver.Start();
            }
        }
        // 赋值 拷贝构造都禁止
        ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
        ThreadPool(const ThreadPool<T> &) = delete;

    public:
        // 提供一个接口:线程安全的单例模式(双检锁 Double-Checked Locking)
        static ThreadPool<T> *Instance()
        {
            if (nullptr == _instance)
            {
                LockGuard lock(&_mutex_instance);
                if (nullptr == _instance)
                {
                    _instance = new ThreadPool<T>();
                    std::cout << "第一次使用线程池,创建线程池对象";
                }
            }
            return _instance;
        }
        void Stop()
        {
            {
                LockGuard lock(&_mutex);
                _isRunning = false;
                if (_slaver_sleep_count > 0)
                    _cond.Broadcast();
            }
        }

        void Enqueue(T in)
        {
            LockGuard lock(&_mutex);
            _tasks.push(in);
            if (_slaver_sleep_count > 0)
            {
                _cond.Broadcast();
            }
        }

        ~ThreadPool()
        {
            if (_isRunning)
            {
                Stop();
            }
            for (auto &slaver : _slavers)
            {
                slaver.Join();
            }
        }

    private:
        bool _isRunning;
        int _slaver_num;
        std::vector<Thread> _slavers;
        std::queue<T> _tasks;
        Mutex _mutex;
        Cond _cond;
        int _slaver_sleep_count;

        // 单例模式相关静态成员
        static ThreadPool<T> *_instance;
        static Mutex _mutex_instance;
    };
}
// 类外对静态成员初始化
template <typename T>
ThreadPool<T> *ThreadPool<T>::_instance = nullptr;
template <typename T>
Mutex ThreadPool<T>::_mutex_instance;

四、线程安全和重入问题

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

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

那么如何理解重入和线程安全呢? ------ 在概念上就不同

  • 线程安全的侧重点在并发执行上
  • 重入和不可重入:是函数的特点!

但是二者在操作层面上有交集。必然,线程会调用函数!

学到现在,其实我们已经能理解重入其实可以分为两种情况

  • 多线程重入函数(重点)
  • 信号导致⼀个执行流重复进入函数(不考虑)

结论 :不要被上面绕口令式的话语唬住,你只要仔细观察,其实对应概念说的都是一回事。

五、常见锁概念

🔒死锁

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

为了方便表述,假设现在线程A,线程B必须同时持有锁1和锁2,才能进行后续资源的访问

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

线程A持有锁1,线程B持有锁2;但是二者都想去继续尝试获取他人的锁,最后结果就是线程A和线程B都被挂起了,二者被挂起之后,就不能释放自己所拥有的锁了 ------ 死锁

一个线程一把锁,有可能出现死锁吗?------ 有可能,自己的锁没有释放就又申请了一把锁,自己绊自己

🔒死锁的四个必要条件

必要条件 :要产生死锁,这四个条件必须同时满足,只要破坏一个条件就是在破坏死锁了

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

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

不剥夺条件 :⼀个执行流已获得的资源,在末使用完之前,不能强行剥夺(不准抢锁)

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

🔒避免死锁

避免死锁最好的解决办法就是不加锁 ------ 工程问题里能不加锁就不加

破坏死锁的四个必要条件

比如说破坏请求与保持条件 ,一般是破坏保持条件,只要我一申请锁失败了就就直接把自己当前的锁给释放掉 ------ 破坏了保持条件

破坏不剥夺条件:给线程设置优先级,线程A优先级比线程B高就,允许申请锁2。也就破坏了不剥夺条件

破坏循环等待条件问题资源一次性分配,使用超时机制、加锁顺序一致

  • 锁1 和 锁2 都让一个人原子性的申请了

因为申请两把锁,不是原子的,解决方法:std::lock原子性的方式同时持有这两把锁

  • 本质上是先申请锁3, 谁申请锁3成功了,就能拥有锁1和锁2
  • 一次性锁住多个互斥量,并且保证不会发生死锁
  • 要么全部锁成功、要么全部不锁、绝不能死锁

🔒避免死锁算法

死锁检测算法、银行家算法(了解)

六、STL,智能指针和线程安全

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

  • 不是.原因是,STL设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶).
  • 因此STL默认不是线程安全。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全.

智能指针是否是线程安全的?

  • 对于unique_ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题
  • 对于shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题 。但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证shared_ptr能够高效,原子的操作引用计数.
  • 智能指针所指向的对象有可能是线程不安全的

📢写在最后

接下来是 Linux 网络基础 ------ 马刺👽冲冲冲🚀

相关推荐
AOwhisky2 小时前
MySQL 学习笔记(第七期):高可用架构进阶与综合项目实战
linux·运维·笔记·学习·mysql·高可用·mha
无限进步_2 小时前
【Linux】进程状态、僵尸与孤儿、进程调度
linux·运维·服务器·开发语言·数据结构·算法
郝学胜-神的一滴2 小时前
力扣 662 :二叉树最大宽度
java·数据结构·c++·python·算法·leetcode·职场和发展
着迷不白2 小时前
七、Linux网络管理
服务器·网络·php
老码观察2 小时前
设计模式实战解读(十二):状态模式——干掉状态机里的 if-else 地狱
设计模式·状态模式
Urbano2 小时前
工业及物流工装制作流程与各工序自动化替代方案
运维·自动化
加油码2 小时前
Linux IO 多路转接详解:从 select、poll 到 epoll
linux·c++
是一个Bug2 小时前
Nginx 与 API Gateway:从“小区门卫”到“商场总服务台”
运维·nginx·gateway
syagain_zsx2 小时前
Linux进程控制学习总结(2/2)
linux·运维·学习