《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++ 程序的起始执行点,在这个函数中,通过创建新线程并在主线程和新线程中都获取单例对象实例,来验证在多线程环境下单例模式的正确性和线程安全性。 -
实现步骤:
-
声明
pthread_t
类型的变量id
用于存储线程标识符。pthread_t
是 POSIX 线程库中定义的用于表示线程标识符的类型,通过这个变量来标识和管理创建的线程。 -
调用
pthread_create(&id, 0, thread_main, 0)
创建一个新线程,线程执行函数为thread_main
。pthread_create
函数用于创建一个新的线程,第一个参数是指向线程标识符变量的指针,用于返回新创建线程的标识符;第二个参数是一个指向线程属性对象的指针,这里传递0
表示使用默认属性;第三个参数是新线程要执行的函数指针,即thread_main
函数;第四个参数是传递给新线程执行函数的参数,这里传递0
表示不传递额外参数。 -
在主线程中调用
singleton::constructer()
获取单例对象实例并打印其地址。在主线程中,通过调用singleton
类的constructer
静态方法获取单例对象实例,并将其地址输出到控制台,用于和新线程中获取的单例对象实例地址进行对比。 -
调用
pthread_join(id, 0)
等待创建的线程结束。pthread_join
函数用于阻塞当前线程(这里是主线程),直到指定的线程(通过id
标识)执行完毕。这样可以确保主线程在新线程完成任务后再继续执行后续操作,避免出现主线程提前结束而新线程还未完成任务的情况。 -
再次调用
pthread_create(&id, 0, thread_main, 0)
创建新线程,重复上述获取单例对象实例并打印地址的操作,最后等待该线程结束,程序返回0
。通过再次创建新线程并重复获取单例对象实例的操作,进一步验证在多线程环境下单例模式的可靠性和线程安全性,确保在多次创建线程的情况下,都能正确获取到唯一的单例对象实例。
-
三、代码运行流程
-
程序启动,进入
main
函数。程序从main
函数开始执行,这是 C++ 程序的入口点。 -
创建第一个线程,该线程开始执行
thread_main
函数。在main
函数中,通过调用pthread_create
函数创建一个新线程,新线程启动后开始执行thread_main
函数。 -
在
thread_main
函数中,调用singleton::constructer
获取单例对象。此时如果单例对象尚未创建,constructer
函数会加锁,检查发现only
为NULL
,然后模拟耗时操作(睡眠 1 秒),创建单例对象,最后解锁并返回单例对象实例,线程打印单例对象地址。新线程在执行thread_main
函数时,首先调用singleton
类的constructer
静态方法获取单例对象。如果这是第一次创建单例对象,那么constructer
函数会通过加锁操作保证只有当前线程能够进行创建操作,检查到only
为NULL
后,模拟耗时操作(这里是睡眠 1 秒),然后创建单例对象,最后解锁并返回创建好的单例对象实例,新线程将该实例的地址打印出来。 -
在主线程中,也调用
singleton::constructer
获取单例对象,由于此时单例对象已被创建,所以直接返回已创建的对象实例并打印地址。在主线程中,同样调用singleton
类的constructer
静态方法获取单例对象,因为此时单例对象已经在新线程中被创建,所以constructer
函数直接返回已创建的单例对象实例,并将其地址打印出来。 -
主线程调用
pthread_join
等待第一个线程结束。主线程通过调用pthread_join
函数,阻塞自身直到第一个创建的新线程执行完毕,确保主线程和新线程之间的执行顺序和同步性。 -
再次创建第二个线程,该线程同样执行
thread_main
函数获取单例对象,由于单例对象已存在,直接返回并打印地址。在第一个新线程结束后,主线程再次创建一个新线程,这个新线程也执行thread_main
函数来获取单例对象。由于单例对象已经存在,所以constructer
函数直接返回已存在的单例对象实例,并将其地址打印出来。 -
主线程再次调用
pthread_join
等待第二个线程结束,程序结束。主线程再次通过调用pthread_join
函数,等待第二个新线程执行完毕,然后程序结束运行。 -
在
singleton
类中新增成员变量isDestroyed
:用于标记单例对象是否已经被销毁,初始化为false
。这个变量的作用是在多个线程同时尝试销毁单例对象或者在程序运行过程中多次判断单例对象是否已销毁时,提供一个准确的状态标识,避免重复销毁或者误判。 -
新增
destroy
静态函数:-
首先调用
m.lock()
加锁,确保在销毁单例对象的过程中,不会有其他线程同时进行相关操作。 -
然后检查
only
是否为NULL
并且isDestroyed
是否为false
,如果满足条件,说明单例对象存在且尚未被销毁,此时调用delete only
释放单例对象占用的内存,将only
指针设为NULL
,并将isDestroyed
设为true
。 -
最后调用
m.unlock()
解锁,允许其他线程继续执行。
-
-
在
main
函数中新增singleton::destroy()
调用:在主线程等待创建的线程结束后,调用singleton
类的destroy
静态函数来销毁单例对象,释放其占用的资源,避免内存泄漏问题。
四、主要事项
-
代码中使用了 POSIX 线程库,在编译时需要链接该库,例如在 Linux 环境下使用
g++
编译,需添加-pthread
选项(如g++ -o main main.cpp -pthread
) 。POSIX 线程库是一个提供多线程编程接口的库,为了让编译器能够正确链接和使用这个库中的函数和类型,需要在编译时添加-pthread
选项,否则会出现编译错误,提示找不到相关的函数和类型定义。 -
虽然代码实现了基本的线程安全的单例模式,但在实际应用中,还需考虑单例对象的销毁时机和内存管理等问题,目前代码中未涉及单例对象的销毁逻辑。在实际应用中,单例对象可能会占用一些系统资源,如内存、文件句柄等。当程序结束或不再需要单例对象时,需要正确地释放这些资源。目前的代码中没有实现单例对象的销毁逻辑,可能会导致内存泄漏等问题。可以考虑在程序退出时手动释放单例对象占用的资源,或者使用智能指针等技术来自动管理单例对象的生命周期。
-
代码中的
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;
}