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

在 C++11 及以后标准中,多线程编程变得更加简单和安全。实现一个线程安全的单例模式通常有几种方式,下面我们依次讨论:

1. 局部静态变量(Meyers 单例)

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;   // C++11 保证线程安全
        return instance;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() {}
};
  • 优点:实现极简,编译器负责线程同步。
  • 缺点:若需要在对象构造前检查错误,可能不够灵活。

2. std::call_oncestd::once_flag

class Singleton {
public:
    static Singleton& getInstance() {
        std::call_once(initFlag, []() { instance.reset(new Singleton); });
        return *instance;
    }
private:
    Singleton() {}
    static std::unique_ptr <Singleton> instance;
    static std::once_flag initFlag;
};

std::unique_ptr <Singleton> Singleton::instance;
std::once_flag Singleton::initFlag;
  • 优点:可以在构造前做错误处理,初始化逻辑更可控。
  • 缺点:代码稍显繁琐。

3. 双重检查锁(DCLP)— 传统实现

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

Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;
  • 优点:对老旧编译器兼容。
  • 缺点:若不小心实现不对,容易出现指令重排导致线程不安全。C++11 的内存模型已经足够安全,通常不建议手写 DCLP。

4. std::shared_ptrstd::weak_ptr(懒加载与销毁)

class Singleton {
public:
    static std::shared_ptr <Singleton> getInstance() {
        std::call_once(initFlag, []() { 
            instance = std::shared_ptr <Singleton>(new Singleton);
        });
        return instance;
    }
private:
    Singleton() {}
    static std::shared_ptr <Singleton> instance;
    static std::once_flag initFlag;
};

std::shared_ptr <Singleton> Singleton::instance;
std::once_flag Singleton::initFlag;
  • 优点:可以自动管理生命周期,支持多处引用。
  • 缺点:需要注意循环引用导致内存泄漏。

5. 采用 std::atomicstd::mutex 的组合(更通用的实现)

class Singleton {
public:
    static Singleton& getInstance() {
        Singleton* tmp = instance.load(std::memory_order_acquire);
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mutex);
            tmp = instance.load(std::memory_order_relaxed);
            if (!tmp) {
                tmp = new Singleton;
                instance.store(tmp, std::memory_order_release);
            }
        }
        return *tmp;
    }
private:
    Singleton() {}
    static std::atomic<Singleton*> instance;
    static std::mutex mutex;
};

std::atomic<Singleton*> Singleton::instance(nullptr);
std::mutex Singleton::mutex;
  • 优点:显式控制内存顺序,满足极高性能要求。
  • 缺点:实现相对复杂,易出错。

何时使用哪种实现?

场景 推荐实现 说明
需要最简洁实现,且不需要在构造时检查错误 Meyers 单例 只需一行代码即可
构造过程中可能抛异常或需要做额外检查 std::call_once 支持错误处理
需要支持多处销毁(计数式) std::shared_ptr 自动管理生命周期
对老旧编译器(C++11 之前)兼容 双重检查锁 需谨慎实现
性能极限(微秒级) std::atomic + std::mutex 细粒度内存顺序控制

常见错误与调试技巧

  1. 双重检查锁未加 std::atomic
    在 C++11 之后,std::atomicstd::memory_order 可保证可见性。若省略,会出现“空指针解引用”的隐蔽错误。

  2. 忘记 delete
    如果使用裸指针,需要在适当时机删除,防止内存泄漏。std::unique_ptrstd::shared_ptr 可以自动完成。

  3. 构造函数抛异常
    std::call_once 在异常后会重新尝试初始化,但需注意 std::once_flag 在异常后仍可重用。

  4. 多线程读写顺序
    通过 std::memory_order_acquire / release 可以细粒度控制访问顺序,避免缓存不一致。

小结

C++11 之后,线程安全单例实现几乎不需要手写锁,而是利用编译器和标准库提供的同步原语。选择合适的实现方式,既能保持代码简洁,又能满足特定需求。掌握这些模式后,写出既安全又高效的单例组件将不再是难题。

发表评论