如何在C++中实现线程安全的单例模式?

单例模式是一种常见的软件设计模式,用于确保一个类只有一个实例,并提供全局访问点。在C++中实现线程安全的单例模式有多种方法,下面介绍几种常用且易于理解的实现方式。

1. 经典Meyers单例(C++11及以后)

C++11引入了对局部静态变量初始化的线程安全保证。最简单、最安全的单例实现就是使用局部静态对象:

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;  // C++11 线程安全初始化
        return instance;
    }

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

private:
    Singleton() = default;
    ~Singleton() = default;
};

优点:

  • 代码简洁,易于维护。
  • 线程安全且延迟初始化。

缺点:

  • 不能在C++11之前的编译器上使用。
  • 如果需要在程序结束前手动销毁实例,默认实现不支持。

2. 带锁的双重检查锁定(Double-Checked Locking)

如果你在旧编译器(C++03)环境中,需要手动实现线程安全的单例,可以使用双重检查锁定结合互斥量:

#include <mutex>

class Singleton {
public:
    static Singleton* instance() {
        if (!ptr_) {                     // 第一检查
            std::lock_guard<std::mutex> lock(mutex_);
            if (!ptr_) {                 // 第二检查
                ptr_ = new Singleton();
            }
        }
        return ptr_;
    }

    ~Singleton() {
        delete ptr_;
    }

private:
    Singleton() = default;
    static Singleton* ptr_;
    static std::mutex mutex_;
};

Singleton* Singleton::ptr_ = nullptr;
std::mutex Singleton::mutex_;

优点:

  • 支持在C++03中使用。
  • 只在第一次实例化时加锁,后续访问更快。

缺点:

  • 需要手动管理内存,容易出现泄漏或双删。
  • 代码相对复杂。

3. 使用std::call_once(C++11)

std::call_once是C++11提供的一种一次性调用机制,可保证某个函数仅执行一次,常用于单例初始化:

#include <mutex>

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(initFlag_, [](){ ptr_ = new Singleton(); });
        return *ptr_;
    }

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

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

    static Singleton* ptr_;
    static std::once_flag initFlag_;
};

Singleton* Singleton::ptr_ = nullptr;
std::once_flag Singleton::initFlag_;

优点:

  • 线程安全,代码相对简洁。
  • 只在第一次调用时执行初始化。

缺点:

  • 仍需要手动管理内存(可改为智能指针)。

4. 智能指针与std::shared_ptr

为了避免手动内存管理,可以结合std::shared_ptrstd::call_once

#include <memory>
#include <mutex>

class Singleton {
public:
    static std::shared_ptr <Singleton> instance() {
        std::call_once(initFlag_, [](){ ptr_ = std::make_shared <Singleton>(); });
        return ptr_;
    }

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

private:
    Singleton() = default;

    static std::shared_ptr <Singleton> ptr_;
    static std::once_flag initFlag_;
};

std::shared_ptr <Singleton> Singleton::ptr_ = nullptr;
std::once_flag Singleton::initFlag_;

优点:

  • 自动管理生命周期,避免泄漏。
  • 线程安全且延迟初始化。

5. 静态局部对象+C++14[[nodiscard]]

C++14提供[[nodiscard]]属性,可强制编译器警告如果忽略返回值,确保单例被正确使用:

class Singleton {
public:
    [[nodiscard]] static Singleton& instance() {
        static Singleton instance;  // 线程安全
        return instance;
    }
    // ...
};

6. 使用模块化编译(C++20)

C++20引入模块化,可将单例定义在模块中,进一步提高编译速度和安全性。示例略。

何时选择哪种实现?

实现方式 适用场景 优缺点
Meyers单例(局部静态) C++11+ 简洁、线程安全
双重检查锁定 C++03 兼容旧编译器,易出错
std::call_once C++11+ 线程安全,易于理解
std::shared_ptr+call_once 需要自动销毁 资源管理安全
[[nodiscard]] 防止误用 语义明确

小结

在现代C++中,最推荐使用Meyers单例std::call_once配合std::shared_ptr的实现。它们既简洁又可靠,充分利用了语言的线程安全特性。若你必须在旧编译器环境中工作,则双重检查锁定是可行但需谨慎的备选方案。通过合适的单例实现,你可以让代码既安全又易于维护。

发表评论