【Linux学习笔记】线程安全问题之单例模式和死锁

【Linux学习笔记】线程安全问题之单例模式和死锁

🔥个人主页大白的编程日记

🔥专栏Linux学习笔记


文章目录

  • 【Linux学习笔记】线程安全问题之单例模式和死锁
    • 前言
    • [3-3 线程安全的单例模式](#3-3 线程安全的单例模式)
    • [4. 线程安全和重入问题](#4. 线程安全和重入问题)
  • [5. 常见锁概念](#5. 常见锁概念)
  • 5-1死锁
  • [6. STL,智能指针和线程安全](#6. STL,智能指针和线程安全)
    • [6-1 STL中的容器是否是线程安全的?](#6-1 STL中的容器是否是线程安全的?)
    • [6-2 智能指针是否是线程安全的?](#6-2 智能指针是否是线程安全的?)
  • [7. 其他常见的各种锁](#7. 其他常见的各种锁)

前言

哈喽,各位小伙伴大家好!上期我们讲了日志器和线程池 今天我们讲的是线程安全问题之单例模式和死锁。话不多说,我们进入正题!向大厂冲锋!

3-3 线程安全的单例模式

3-3-1 什么是单例模式

3-3-2 单例模式的特点

某些类, 只应该具有一个对象(实例), 就称之为单例.

例如一个男人只能有一个媳妇。

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

3-3-3饿汉实现方式和懒汉实现方式

洗碗的例子

1 吃完饭,立刻洗碗,这种就是饿汉方式。因为下一顿吃的时候可以立刻拿着碗就能吃饭。

2 吃完饭,先把碗放下,然后下一顿饭用到这个碗了再洗碗,就是懒汉方式。

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

3-3-4饿汉方式实现单例模式

cpp 复制代码
1 template <typename T>
2 class Singleton {
3     static T data;
4     public:
5         static T* GetInstance() {
6             return &data;
7         }
8     };

只要通过Singleton这个包装类来使用T对象,则一个进程中只有一个T对象的实例.

3-3-5 懒汉方式实现单例模式

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

存在一个严重的问题, 线程不安全.

第一次调用GetInstance的时候,如果两个线程同时调用,可能会创建出两份T对象的实例。

但是后续再次调用,就没有问题了.

3-3-6 懒汉方式实现单例模式(线程安全版本)

cpp 复制代码
#include <mutex>

template <typename T>
class Singleton {
private:
    static T* inst;
    static std::mutex lock;

public:
    static T* GetInstance() {
        if (inst == nullptr) {
            std::lock_guard<std::mutex> guard(lock); // 使用 lock_guard 来自动管理锁
            if (inst == nullptr) {
                inst = new T();
            }
        }
        return inst;
    }

    static void DestroyInstance() {
        std::lock_guard<std::mutex> guard(lock);
        delete inst;
        inst = nullptr;
    }
};

template <typename T>
T* Singleton<T>::inst = nullptr;

template <typename T>
std::mutex Singleton<T>::lock;

注意事项:

  1. 加锁解锁的位置
  2. 双重 if 判定, 避免不必要的锁竞争
  3. volatile关键字防止过度优化

3-4 单例式线程池

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include "Log.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include "Mutex.hpp"
using namespace std;
namespace ThreadPoolModule
{
    using namespace ThreadModlue;
    using namespace LogModule;
    using namespace CondModule;
    using namespace MutexModule;
    static const int gnum = 5;
    template <typename T>
    class ThreadPool
    {
    public:
        // 初始化线程池
        ThreadPool(int num = gnum)
            : _num(num)
        {
            for (int i = 0; i < _num; i++)
            {
                _threads.emplace_back(
                    [this]()
                    {
                        HandlerTask();
                    });
            }
        }
        // 往任务队列push任务
        bool Enqueue(const T &in)
        {
            if (_isrunning)
            {
                LockGuard guard(_mutex);
                _taskq.push(in);
                if (_threads.size() == _sleepernum)
                {
                     WakeUponce();
                }
                return 1;
            }
            return 0;
        }
        // 启动线程池
        void Start()
        {
            if (_isrunning)
            {
                return;
            }
            _isrunning = 1;
            for (auto &x : _threads)
            {
                x.Start();
                LOG(LogLevel::INFO) << "start new thread success: " << x.Name();
            }
        }
        void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while (true)
            {
                T t;
                {
                    LockGuard gurad(_mutex);
                    // 如果任务队列为空线程并且县城是运行状态才休眠
                    // while循环判断防止伪唤醒
                    while (_taskq.empty() && _isrunning)
                    {
                        _sleepernum++;
                        _cond.Wait(_mutex);
                        _sleepernum--;
                    }
                    // 如果线程不运行并且任务队列为空此时线程池退出
                    if (!_isrunning && _taskq.empty())
                    {
                        LOG(LogLevel::INFO) << name << " 退出了, 线程池退出&&任务队列为空";
                        break;
                    }
                    t = _taskq.front();//获取任务
                    _taskq.pop();//弹出任务
                }
                t();//处理任务不需要在临界区处理
            }
        }
        static ThreadPool<T> *GetInstance()
        {
            if (inc == nullptr)
            {
                LockGuard lockguard(_lock);
                LOG(LogLevel::DEBUG) << "获取单例....";
                if (inc == nullptr)
                {
                    LOG(LogLevel::DEBUG) << "首次使用单例, 创建之....";
                    inc = new ThreadPool<T>;
                    inc->Start();
                }
            }
            return inc;
        }
        void WakeUponce()
        {
            _cond.Signal();
            LOG(LogLevel::INFO) << "唤醒一个休眠线程";
        }
        void WakeUpAllThread()
        {
            LockGuard guard(_mutex);
            if (_sleepernum > 0)
            {
                _cond.Brodcast();
            }
            LOG(LogLevel::INFO) << "唤醒所有的休眠线程";
        }
        void Stop()
        {
            if (!_isrunning)
            {
                return;
            }
            _isrunning = 0;
            WakeUpAllThread();
        }
        void Join()
        {
            for (auto x : _threads)
            {
                x.Join();
            }
        }

    private:
        std::vector<Thread> _threads;
        int _num; // 线程池中,线程的个数
        std::queue<T> _taskq;
        Cond _cond; // 条件变量
        Mutex _mutex;
        bool _isrunning;        
        int _sleepernum;
        static ThreadPool<T> *inc; // 单例指针
        static Mutex _lock;
    };
    template <typename T>
    ThreadPool<T> *ThreadPool<T>::inc = nullptr;

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

测试样例代码

cpp 复制代码
#include <iostream>
#include <functional>
#include <unistd.h>
#include "ThreadPool.hpp"

using task_t = std::function<void()>;

void Download()
{
    std::cout << "this is a task" << std::endl;
}

int main()
{
    ENABLE_CONSOLE_LOG_STRATEGY();

    int cnt = 10;
    while(cnt)
    {
        ThreadPool<task_t>::GetInstance()->Enqueue(Download);
        sleep(1);
        cnt--;
    }

    ThreadPool<task_t>::GetInstance()->Stop();
    sleep(5);
    ThreadPool<task_t>::GetInstance()->Wait();

    return 0;
}
cpp 复制代码
1 $ ./a.out  
2 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [28] - ThreadPool Construct()  
3 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread Thread-0 done  
4 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread Thread-1 done  
5 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread Thread-2 done  
6 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread Thread-3 done  
7 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread Thread-4 done  
8 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread Thread-5 done  
9 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread Thread-6 done  
10 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread Thread-7 done  
11 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread Thread-8 done  
12 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [36] - init thread Thread-9 done  
13 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread Thread-0done  
14 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread Thread-1done
15 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [51] - Thread-0 is running...  
16 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread Thread-2done  
17 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread Thread-3done  
18 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [51] - Thread-2 is running...  
19 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread Thread-4done  
20 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [51] - Thread-3 is running...  
21 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread Thread-5done  
22 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [51] - Thread-4 is running...  
23 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [51] - Thread-5 is running...  
24 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread Thread-6done  
25 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [51] - Thread-6 is running...  
26 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread Thread-7done  
27 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [51] - Thread-7 is running...  
28 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread Thread-8done  
29 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [45] - start thread Thread-9done  
30 [2024-08-04 15:03:37] [DEBUG] [206234] [ThreadPool.hpp] [98] - 创建线程池单例  
31 [2024-08-04 15:03:37] [DEBUG] [206234] [ThreadPool.hpp] [133] - 任务入队列成功  
32 [2024-08-04 15:03:37] [INFO] [206234] [ThreadPool.hpp] [51] - Thread-1 is running...  
33 [2024-08-04 15:03:37] [DEBUG] [206234] [ThreadPool.hpp] [75] - Thread-0 get a task  
34 this is a task 35  
36 [2024-08-04 15:03:47] [DEBUG] [206234] [ThreadPool.hpp] [102] - 获取线程池单例  
37 [2024-08-04 15:03:47] [DEBUG] [206234] [ThreadPool.hpp] [112] - 线程池退出中...  
38 [2024-08-04 15:03:52] [DEBUG] [206234] [ThreadPool.hpp] [102] - 获取线程池单例  
39 [2024-08-04 15:03:52] [INFO] [206234] [ThreadPool.hpp] [119] - Thread-0 退出...  
40 [2024-08-04 15:03:52] [INFO] [206234] [ThreadPool.hpp] [119] - Thread-1 退出...  
41 [2024-08-04 15:03:52] [INFO] [206234] [ThreadPool.hpp] [119] - Thread-2 退出...

42 [2024-08-04 15:03:52] [INFO] [206234] [ThreadPool.hpp] [119] - Thread-3 退出...  
43 [2024-08-04 15:03:52] [INFO] [206234] [ThreadPool.hpp] [119] - Thread-4 退出...  
44 [2024-08-04 15:03:52] [INFO] [206234] [ThreadPool.hpp] [119] - Thread-5 退出...  
45 [2024-08-04 15:03:52] [INFO] [206234] [ThreadPool.hpp] [119] - Thread-6 退出...  
46 [2024-08-04 15:03:52] [INFO] [206234] [ThreadPool.hpp] [119] - Thread-7 退出...  
47 [2024-08-04 15:03:52] [INFO] [206234] [ThreadPool.hpp] [119] - Thread-8 退出...  
48 [2024-08-04 15:03:52] [INFO] [206234] [ThreadPool.hpp] [119] - Thread-9 退出...

4. 线程安全和重入问题

概念

线程安全:就是多个线程在访问共享资源时,能够正确地执行,不会相互干扰或破坏彼此的执行结果。一般而言,多个线程并发同一段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进行操作,并且没有锁保护的情况下,容易出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

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

  • 多线程重入函数

  • 信号导致一个执行流重复进入函数

  • 常见的线程不安全的情况

  • 不保护共享变量的函数

  • 函数状态随着被调用,状态发生变化的函数

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

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

  • 常见不可重入的情况

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

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

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

  • 常见可重入的情况

  • 不使用全局变量或静态变量

  • 不使用用malloc或者new开辟出的空间

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

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

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

  • 不调用不可重入函数

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

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

bash 复制代码
结论
  • 不要被上面绕口令式的话语唬住,你只要仔细观察,其实对应概念说的都是一回事。
bash 复制代码
可重入与线程安全联系
  • 函数是可重入的,那就是线程安全的(其实知道这一句话就够了)
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
bash 复制代码
 可重入与线程安全区别

可重入函数是线程安全函数的一种

线程安全不一定是可重入的,而可重入函数则一定是线程安全的。

如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

注意:

  • 如果不考虑信号导致一个执行流重复进入函数这种重入情况,线程安全和重入在安全角度不做区分
  • 但是线程安全侧重说明线程访问公共资源的安全情况,表现的是并发线程的特点
  • 可重入描述的是一个函数是否能被重复进入,表示的是函数的特点

5. 常见锁概念

5-1死锁

  • 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。
  • 为了方便表述,假设现在线程A,线程B必须同时持有锁1和锁2,才能进行后续资源的访问

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

造成的结果是

5-2 死锁四个必要条件

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

好理解,不做解释

  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在未使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

5-3避免死锁

  • 破坏死锁的四个必要条件

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

cpp 复制代码
1 //下面的C++不写了,理解就可以  
2  
3 #include<iostream>  
4 #include <texx>  
5 #include <thread>  
6 #include <vector>  
7 #include <unistd.h>  
8  
9 //定义两个共享资源(整数变量)和两个互斥锁  
10 int shared_resource1 = 0;  
11 int shared_resource2 = 0;  
12 std::texm mtx1, mtx2;  
13  
14 //一个函数,同时访问两个共享资源  
15 void access_shared-resources()  
16 {  
17 // std::unique_lock<std::texx> lock1(mtx1, std::defer_lock);  
18 // std::unique_lock<std::texx> lock2(mtx2, std::defer_lock);  
19 //使用std::lock同时锁定两个互斥锁  
20 //std::lock(lock1, lock2);  
21  
22 //现在两个互斥锁都已锁定,可以安全地访问共享资源  
23 int cnt = 10000;  
24 while (cnt)  
25 {  
26 ++sharedResource1;  
27 ++sharedResource2;  
28 cnt--;  
29 }  
30  
31 //当离开access_shared-resources的作用域时,lock1和lock2的析构函数会被自动调用  
32 //这会导致它们各自的互斥量被自动解锁  
33 }  
34  
35 //模拟多线程同时访问共享资源的场景  
36 void simulate_concurrent_access()  
37 {  
38     std::vector<std::thread> threads;
39  }
40 //创建多个线程来模拟并发访问   
41 for (int i = 0; i < 10; ++i)   
42 { 
     threads'emplace_back(access_shared-resources);   
43 }   
45   
46 //等待所有线程完成   
47 for (auto &thread : threads)   
48 { 
      thread.join();   
49 }   
50   
51   
52 //输出共享资源的最终状态   
53 std::cout << "Shared Resource 1: " << shared_resource1 << std::endl;   
54 std::cout << "Shared Resource 2: " << shared_resource2 << std::endl;   
55 }   
56   
57 int main()   
58 {   
59   simulate_concurrent_access();   
60   return 0;   
61 }
bash 复制代码
1 $ ./a.out // 不一次申请  
2 Shared Resource 1: 94416  
3 Shared Resource 2: 94536

1 $ ./a.out // 一次申请  
2 Shared Resource 1: 100000  
3 Shared Resource 2:100000

避免锁未释放的场景

5-4避免死锁算法

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

6. STL,智能指针和线程安全

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

不是.

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

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

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

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

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

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

7. 其他常见的各种锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。

后言

这就是线程安全问题之单例模式和死锁。大家自己好好消化!今天就分享到这! 感谢各位的耐心垂阅!咱们下期见!拜拜~

相关推荐
超级大只老咪2 小时前
快速进制转换
笔记·算法
嵩山小老虎3 小时前
Windows 10/11 安装 WSL2 并配置 VSCode 开发环境(C 语言 / Linux API 适用)
linux·windows·vscode
Fleshy数模3 小时前
CentOS7 安装配置 MySQL5.7 完整教程(本地虚拟机学习版)
linux·mysql·centos
a41324473 小时前
ubuntu 25 安装vllm
linux·服务器·ubuntu·vllm
Fᴏʀ ʏ꯭ᴏ꯭ᴜ꯭.5 小时前
Keepalived VIP迁移邮件告警配置指南
运维·服务器·笔记
一只自律的鸡5 小时前
【Linux驱动】bug处理 ens33找不到IP
linux·运维·bug
17(无规则自律)5 小时前
【CSAPP 读书笔记】第二章:信息的表示和处理
linux·嵌入式硬件·考研·高考
!chen5 小时前
linux服务器静默安装Oracle26ai
linux·运维·服务器
ling___xi5 小时前
《计算机网络》计网3小时期末速成课各版本教程都可用谢稀仁湖科大版都可用_哔哩哔哩_bilibili(笔记)
网络·笔记·计算机网络