当前位置: 首页 > news >正文

《C++多线程下单例 “锁钥” 法则》

一、概述

        本文章介绍了一段 C++ 代码,该代码实现了在多线程环境下的单例模式。单例模式确保一个类只有一个实例,并提供全局访问点。在多线程场景中,需要额外的同步机制来保证单例对象创建的线程安全性。单例模式在许多场景中都有重要应用,比如资源管理、配置管理等,它能有效避免资源的重复创建和不一致问题。

二、代码结构与功能模块

(一)Mutex 类

  • 功能:封装了 POSIX 线程库(pthread)中的互斥锁操作,用于实现线程同步。互斥锁是多线程编程中常用的同步工具,它可以保证在同一时刻只有一个线程能够访问被保护的临界区代码,防止数据竞争和不一致问题。

  • 成员变量

    • pthread_mutex_t m:用于表示互斥锁的内部变量。pthread_mutex_t 是 POSIX 线程库中定义的互斥锁类型,通过这个变量来存储和管理互斥锁的状态。

  • 成员函数

  • Mutex():构造函数,调用 pthread_mutex_init(&m, 0) 初始化互斥锁。在对象创建时,通过这个函数对互斥锁进行初始化操作,为后续的加锁和解锁操作做准备。其中 pthread_mutex_init 函数的第一个参数是指向要初始化的互斥锁变量的指针,第二个参数是一个指向属性对象的指针,这里传递 0 表示使用默认属性。

  • void lock():加锁函数,调用 pthread_mutex_lock(&m) 对互斥锁进行加锁操作,阻止其他线程同时访问受保护的资源。当一个线程调用这个函数时,如果互斥锁当前处于未锁定状态,那么该线程将获得锁并继续执行;如果互斥锁已经被其他线程锁定,那么当前线程将被阻塞,直到获得锁为止。

  • void unlock():解锁函数,调用 pthread_mutex_unlock(&m) 释放互斥锁,允许其他等待的线程获取锁并访问资源。当一个线程完成对临界区代码的操作后,需要调用这个函数释放互斥锁,以便其他被阻塞的线程有机会获取锁并继续执行。

(二)singleton 类

  • 功能:实现单例模式,确保只有一个该类的实例存在,并提供获取该实例的静态方法。单例模式的核心目的是保证在整个应用程序中,一个类只有一个实例,并且可以通过一个全局的访问点来获取这个实例。

  • 成员变量

    • static singleton* only:静态指针,用于存储唯一的单例对象实例,初始化为 NULL。这个指针在整个程序运行期间保持对单例对象的引用,通过它来实现对单例对象的唯一访问。

    • static Mutex m:静态互斥锁对象,用于在多线程创建单例对象时进行同步。由于在多线程环境下,可能会有多个线程同时尝试创建单例对象,所以需要使用互斥锁来保证只有一个线程能够成功创建对象,避免出现多个实例的情况。

  • 成员函数

    • singleton():私有构造函数,输出 “唯构造函数”,防止外部直接实例化该类。将构造函数设为私有,是单例模式的关键实现方式之一,这样外部代码就无法通过 new 操作直接创建该类的对象,只能通过类提供的静态方法来获取唯一的实例。

    • static singleton* constructer():静态函数,用于获取单例对象实例。首先调用 m.lock() 加锁,然后检查 only 是否为 NULL,若为 NULL,则模拟耗时操作(通过 sleep(1))后创建单例对象,最后调用 m.unlock() 解锁并返回单例对象实例。加锁操作是为了确保在检查和创建单例对象的过程中,不会有其他线程同时进行相同的操作,从而保证单例对象的唯一性。检查 only 是否为 NULL 是判断单例对象是否已经被创建,如果尚未创建,则进行创建操作。模拟耗时操作(这里使用 sleep(1))是为了更真实地模拟在实际场景中可能出现的一些复杂初始化逻辑,在实际应用中,这个部分可能会涉及到数据库连接初始化、文件读取等耗时操作。创建完单例对象后,通过解锁操作释放互斥锁,允许其他线程继续尝试获取单例对象。

(三)thread_main 函数

  • 功能:作为线程执行函数,用于在新线程中获取单例对象实例并打印其地址。在多线程编程中,每个线程都有自己的执行函数,thread_main 函数就是为新创建的线程指定的执行函数,它负责在新线程的上下文中完成获取单例对象实例并进行相关操作(这里是打印地址)的任务。

  • 实现:通过调用 singleton::constructer() 获取单例对象实例,将其存储在指针 s 中,然后使用 std::cout 打印该指针的值(即单例对象的地址),最后返回 0 表示线程正常结束。当新线程启动后,会执行这个函数,首先调用 singleton 类的 constructer 静态方法获取单例对象实例,然后通过 std::cout 将该实例的地址输出到控制台,这样可以直观地看到在不同线程中获取到的是否是同一个单例对象实例。最后返回 0 是一种常见的线程执行函数返回值约定,表示线程正常执行完毕。

(四)main 函数

  • 功能:程序的入口点,负责创建线程并在主线程中获取单例对象实例,展示多线程环境下单例模式的应用。main 函数是 C++ 程序的起始执行点,在这个函数中,通过创建新线程并在主线程和新线程中都获取单例对象实例,来验证在多线程环境下单例模式的正确性和线程安全性。

  • 实现步骤

    1. 声明 pthread_t 类型的变量 id 用于存储线程标识符。pthread_t 是 POSIX 线程库中定义的用于表示线程标识符的类型,通过这个变量来标识和管理创建的线程。

    2. 调用 pthread_create(&id, 0, thread_main, 0) 创建一个新线程,线程执行函数为 thread_mainpthread_create 函数用于创建一个新的线程,第一个参数是指向线程标识符变量的指针,用于返回新创建线程的标识符;第二个参数是一个指向线程属性对象的指针,这里传递 0 表示使用默认属性;第三个参数是新线程要执行的函数指针,即 thread_main 函数;第四个参数是传递给新线程执行函数的参数,这里传递 0 表示不传递额外参数。

    3. 在主线程中调用 singleton::constructer() 获取单例对象实例并打印其地址。在主线程中,通过调用 singleton 类的 constructer 静态方法获取单例对象实例,并将其地址输出到控制台,用于和新线程中获取的单例对象实例地址进行对比。

    4. 调用 pthread_join(id, 0) 等待创建的线程结束。pthread_join 函数用于阻塞当前线程(这里是主线程),直到指定的线程(通过 id 标识)执行完毕。这样可以确保主线程在新线程完成任务后再继续执行后续操作,避免出现主线程提前结束而新线程还未完成任务的情况。

    5. 再次调用 pthread_create(&id, 0, thread_main, 0) 创建新线程,重复上述获取单例对象实例并打印地址的操作,最后等待该线程结束,程序返回 0。通过再次创建新线程并重复获取单例对象实例的操作,进一步验证在多线程环境下单例模式的可靠性和线程安全性,确保在多次创建线程的情况下,都能正确获取到唯一的单例对象实例。

三、代码运行流程

  1. 程序启动,进入 main 函数。程序从 main 函数开始执行,这是 C++ 程序的入口点。

  2. 创建第一个线程,该线程开始执行 thread_main 函数。在 main 函数中,通过调用 pthread_create 函数创建一个新线程,新线程启动后开始执行 thread_main 函数。

  3. 在 thread_main 函数中,调用 singleton::constructer 获取单例对象。此时如果单例对象尚未创建,constructer 函数会加锁,检查发现 only 为 NULL,然后模拟耗时操作(睡眠 1 秒),创建单例对象,最后解锁并返回单例对象实例,线程打印单例对象地址。新线程在执行 thread_main 函数时,首先调用 singleton 类的 constructer 静态方法获取单例对象。如果这是第一次创建单例对象,那么 constructer 函数会通过加锁操作保证只有当前线程能够进行创建操作,检查到 only 为 NULL 后,模拟耗时操作(这里是睡眠 1 秒),然后创建单例对象,最后解锁并返回创建好的单例对象实例,新线程将该实例的地址打印出来。

  4. 在主线程中,也调用 singleton::constructer 获取单例对象,由于此时单例对象已被创建,所以直接返回已创建的对象实例并打印地址。在主线程中,同样调用 singleton 类的 constructer 静态方法获取单例对象,因为此时单例对象已经在新线程中被创建,所以 constructer 函数直接返回已创建的单例对象实例,并将其地址打印出来。

  5. 主线程调用 pthread_join 等待第一个线程结束。主线程通过调用 pthread_join 函数,阻塞自身直到第一个创建的新线程执行完毕,确保主线程和新线程之间的执行顺序和同步性。

  6. 再次创建第二个线程,该线程同样执行 thread_main 函数获取单例对象,由于单例对象已存在,直接返回并打印地址。在第一个新线程结束后,主线程再次创建一个新线程,这个新线程也执行 thread_main 函数来获取单例对象。由于单例对象已经存在,所以 constructer 函数直接返回已存在的单例对象实例,并将其地址打印出来。

  7. 主线程再次调用 pthread_join 等待第二个线程结束,程序结束。主线程再次通过调用 pthread_join 函数,等待第二个新线程执行完毕,然后程序结束运行。

  8. 在 singleton 类中新增成员变量 isDestroyed:用于标记单例对象是否已经被销毁,初始化为 false。这个变量的作用是在多个线程同时尝试销毁单例对象或者在程序运行过程中多次判断单例对象是否已销毁时,提供一个准确的状态标识,避免重复销毁或者误判。

  9. 新增 destroy 静态函数:

    • 首先调用 m.lock() 加锁,确保在销毁单例对象的过程中,不会有其他线程同时进行相关操作。

    • 然后检查 only 是否为 NULL 并且 isDestroyed 是否为 false,如果满足条件,说明单例对象存在且尚未被销毁,此时调用 delete only 释放单例对象占用的内存,将 only 指针设为 NULL,并将 isDestroyed 设为 true

    • 最后调用 m.unlock() 解锁,允许其他线程继续执行。

  10. 在 main 函数中新增 singleton::destroy() 调用:在主线程等待创建的线程结束后,调用 singleton 类的 destroy 静态函数来销毁单例对象,释放其占用的资源,避免内存泄漏问题。

四、主要事项

  1. 代码中使用了 POSIX 线程库,在编译时需要链接该库,例如在 Linux 环境下使用 g++ 编译,需添加 -pthread 选项(如 g++ -o main main.cpp -pthread) 。POSIX 线程库是一个提供多线程编程接口的库,为了让编译器能够正确链接和使用这个库中的函数和类型,需要在编译时添加 -pthread 选项,否则会出现编译错误,提示找不到相关的函数和类型定义。

  2. 虽然代码实现了基本的线程安全的单例模式,但在实际应用中,还需考虑单例对象的销毁时机和内存管理等问题,目前代码中未涉及单例对象的销毁逻辑。在实际应用中,单例对象可能会占用一些系统资源,如内存、文件句柄等。当程序结束或不再需要单例对象时,需要正确地释放这些资源。目前的代码中没有实现单例对象的销毁逻辑,可能会导致内存泄漏等问题。可以考虑在程序退出时手动释放单例对象占用的资源,或者使用智能指针等技术来自动管理单例对象的生命周期。

  3. 代码中的 sleep(1) 仅为简单模拟耗时操作,实际场景中可能存在更复杂的业务逻辑,要确保加锁和解锁操作能正确保护相关资源。在实际应用中,单例对象的创建过程可能会涉及到数据库连接、文件读取、网络请求等复杂的操作,这些操作可能会比简单的睡眠操作耗时更长,并且可能会出现各种异常情况。因此,在编写实际的业务逻辑时,需要仔细考虑加锁和解锁的范围和时机,确保在整个操作过程中,互斥锁能够正确地保护相关资源,避免出现数据竞争和不一致问题。同时,还需要处理可能出现的异常情况,保证程序的健壮性。

五、代码实现展示

六、源代码

#include <iostream>
#include <pthread.h>
#include <unistd.h> // 包含 sleep 函数所需的头文件

// 定义一个互斥锁类
class Mutex {
public:
    // 内部使用pthread_mutex_t来表示互斥锁
    pthread_mutex_t m;

public:
    // 构造函数,初始化互斥锁
    Mutex() {
        pthread_mutex_init(&m, 0);
    }
    // 加锁函数
    void lock() {
        pthread_mutex_lock(&m);
    }
    // 解锁函数
    void unlock() {
        pthread_mutex_unlock(&m);
    }

    // 析构函数,销毁互斥锁
    ~Mutex() {
        pthread_mutex_destroy(&m);
    }
};

// 单例类
class singleton {
private:
    // 私有构造函数,防止外部直接实例化
    singleton() {
        std::cout << "唯一构造函数" << std::endl;
    }
    // 静态成员,用于存储唯一的单例对象实例
    static singleton* only;
    // 静态互斥锁对象,用于在多线程创建单例对象时进行同步
    static Mutex m;

    // 静态成员,用于标记单例对象是否已被销毁
    static bool isDestroyed;

public:
    // 静态函数,用于获取单例对象实例
    static singleton* constructer() {
        m.lock();
        try {
            if (only == NULL &&!isDestroyed) {
                sleep(1);
                only = new singleton;
            }
        } catch (...) {
            m.unlock();
            throw;
        }
        m.unlock();
        return only;
    }

    // 静态函数,用于销毁单例对象
    static void destroy() {
        m.lock();
        if (only != NULL && !isDestroyed) {
            delete only;
            only = NULL;
            isDestroyed = true;
        }
        m.unlock();
    }

    ~singleton() {
        std::cout << "单例对象析构函数" << std::endl;
    }
};

// 初始化单例对象指针为NULL
singleton* singleton::only = NULL;
// 初始化静态互斥锁对象
Mutex singleton::m;
// 初始化单例对象销毁标记为false
bool singleton::isDestroyed = false;

// 线程执行函数
void* thread_main(void* arg) {
    // 获取单例对象实例
    singleton* s = singleton::constructer();
    std::cout << s << std::endl;
    return 0;
}

int main(int argc, const char** argv) {
    pthread_t id;
    // 创建线程
    pthread_create(&id, 0, thread_main, 0);

    // 获取单例对象实例(主线程也获取)
    singleton* s = singleton::constructer();
    std::cout << s << std::endl;

    // 等待线程结束
    pthread_join(id, 0);

    // 销毁单例对象
    singleton::destroy();

    return 0;
}

相关文章:

  • Fast-Poly-2024
  • GodWork 3D 7.24 GodWork AT 7.24天工三维实景三维建模软件
  • 技术与情感交织的一生 (四)
  • 树莓集团引领数字产业生态构建的新力量
  • 汇编学习之《指针寄存器大小端学习》
  • 题解:P8628 [蓝桥杯 2015 国 AC] 穿越雷区
  • [Lc5_dfs+floodfill] 岛屿的最大面积(传参) | 被围绕的区域 | 太平洋大西洋水流问题(双标记位传参)
  • # 基于OpenCV的图像拼接与文档检测:从特征提取到透视变换
  • 一致性hash应用-分库分表
  • github 页面超时解决方法
  • ai画图hiresfix放大算法。
  • 蓝桥杯每天5题
  • SQL注入:基于GET和POST的报错注入详解
  • 【含文档+PPT+源码】基于微信小程序的在线考试与选课教学辅助系统
  • RAG 优化 Embedding 模型或调整检索策略
  • VBA代码解决方案第二十三讲 EXCEL中,如何删除工作表中的空白行
  • XSLT Apply:深入解析XSLT在XML转换中的应用
  • Qt之QTextEdit控制文本滚动, 停止滚动, 开始滚动, 鼠标控制滚动
  • 单调队列-滑动窗口算法一篇学会-AcWing 154. 滑动窗口
  • js中的document.querySelect()
  • 做网站用什么地图好/如何做好企业网站的推广
  • 电子商务网站建设的规划书/好看的友情链接代码
  • 电脑上做任务赚钱的网站/网络平台营销
  • 团建拓展网站建设需求分析/网站快速排名推荐
  • 做网站推广赚钱吗/百度推广用户注册
  • 苏州公司建站/重庆 seo