C++多线程安全单例实现详解

在现代C++中,单例模式经常被用于提供全局访问点,尤其是在需要跨线程共享资源时。实现一个线程安全、延迟初始化且开销极小的单例,一直是程序员关注的热点。本文从需求出发,梳理几种常见实现,重点探讨C++11及其后版本的线程安全特性,最后给出完整可直接使用的代码示例。

1. 需求背景

  • 全局共享:某些资源(如日志系统、配置管理器)在整个应用生命周期内只需要创建一次。
  • 延迟初始化:避免在程序启动时就完成昂贵的初始化,只有真正需要时才构造对象。
  • 线程安全:多线程环境下,必须保证单例对象只被构造一次且不会出现竞态条件。

2. 传统实现(非线程安全)

class LegacySingleton {
public:
    static LegacySingleton& instance() {
        static LegacySingleton* p = new LegacySingleton();
        return *p;
    }
private:
    LegacySingleton() = default;
    LegacySingleton(const LegacySingleton&) = delete;
    LegacySingleton& operator=(const LegacySingleton&) = delete;
};

上述实现只在第一次调用instance()时创建对象,随后直接返回已存在的对象。然而,在多线程同时访问时,两个线程可能同时进入instance()函数,导致产生两个实例,破坏单例性质。

3. Meyers Singleton(C++11之后线程安全)

C++11引入了对静态局部变量初始化的线程安全保证。

class MeyersSingleton {
public:
    static MeyersSingleton& instance() {
        static MeyersSingleton inst;  // 线程安全初始化
        return inst;
    }
private:
    MeyersSingleton() = default;
    MeyersSingleton(const MeyersSingleton&) = delete;
    MeyersSingleton& operator=(const MeyersSingleton&) = delete;
};
  • 优点:代码简洁,延迟初始化,线程安全。
  • 缺点:若构造函数抛异常,后续调用将再次尝试构造,直到成功。
  • 适用场景:大多数情况已足够。

4. std::call_once + std::once_flag

如果你需要在构造对象之前执行一次性初始化操作,或者在C++11之前的环境中(比如某些嵌入式编译器),可以使用std::call_once

class CallOnceSingleton {
public:
    static CallOnceSingleton& instance() {
        std::call_once(initFlag, [](){ pInstance = new CallOnceSingleton(); });
        return *pInstance;
    }
private:
    CallOnceSingleton() = default;
    ~CallOnceSingleton() { delete pInstance; }
    static CallOnceSingleton* pInstance;
    static std::once_flag initFlag;
    CallOnceSingleton(const CallOnceSingleton&) = delete;
    CallOnceSingleton& operator=(const CallOnceSingleton&) = delete;
};

CallOnceSingleton* CallOnceSingleton::pInstance = nullptr;
std::once_flag CallOnceSingleton::initFlag;
  • 优点:可控制初始化过程,例如先加载配置再实例化。
  • 缺点:手动管理内存,需要在程序结束时显式删除或使用std::unique_ptr

5. C++17 inline 变量(更简洁)

C++17引入了inline变量,允许在头文件中定义静态成员,减少一次性初始化的复杂度。

class InlineSingleton {
public:
    static InlineSingleton& instance() {
        return inst;
    }
private:
    InlineSingleton() = default;
    static inline InlineSingleton inst;  // inline 变量
    InlineSingleton(const InlineSingleton&) = delete;
    InlineSingleton& operator=(const InlineSingleton&) = delete;
};

这与Meyers Singleton在功能上等价,但显式声明了静态成员,方便阅读。

6. 完整代码示例(线程安全+异常安全+RAII)

#include <iostream>
#include <mutex>

class SafeSingleton {
public:
    static SafeSingleton& getInstance() {
        // 通过call_once保证只调用一次构造
        std::call_once(initFlag, [](){
            instance.reset(new SafeSingleton);
        });
        return *instance;
    }

    // 示例接口
    void doSomething() {
        std::lock_guard<std::mutex> lock(mutex_);
        std::cout << "Singleton instance address: " << this << std::endl;
    }

private:
    SafeSingleton() = default;
    ~SafeSingleton() = default;

    // 禁止拷贝和赋值
    SafeSingleton(const SafeSingleton&) = delete;
    SafeSingleton& operator=(const SafeSingleton&) = delete;

    static std::once_flag initFlag;
    static std::unique_ptr <SafeSingleton> instance;
    std::mutex mutex_;
};

std::once_flag SafeSingleton::initFlag;
std::unique_ptr <SafeSingleton> SafeSingleton::instance = nullptr;

使用示例

#include <thread>

void worker() {
    SafeSingleton::getInstance().doSomething();
}

int main() {
    std::thread t1(worker);
    std::thread t2(worker);
    t1.join();
    t2.join();
    return 0;
}

运行结果显示两次调用输出相同的地址,证明只创建了一个实例。

7. 性能对比

  • Meyers Singleton:最简洁,几乎无额外开销,构造时有一次原子检查。
  • std::call_once:多线程场景下多了一次锁/信号量的开销,但在单例已创建后几乎无额外成本。
  • inline 变量:与Meyers等价,但更易维护。

根据实际需求,若仅需单例,Meyers实现已足够;若需要在单例构造前做一次性初始化或在C++11前环境,可选call_once

8. 结论

  • 推荐:在C++11及以后,优先使用Meyers Singleton或inline变量实现;只在特殊初始化需求时使用std::call_once
  • 注意:若单例在销毁时需要执行清理,务必使用RAII或显式删除,避免内存泄漏。
  • 测试:在多线程环境下,建议用工具(如ThreadSanitizer)验证单例实现的线程安全性。

通过本文的对比与示例,读者可以快速选择合适的单例实现,并在项目中稳健使用。

发表评论