0

0

C++中如何实现单例模式 线程安全与延迟初始化最佳实践

P粉602998670

P粉602998670

发布时间:2025-07-18 12:00:03

|

1043人浏览过

|

来源于php中文网

原创

最推荐在c++++11及更高版本中使用静态局部变量实现线程安全且延迟创建的单例。1. 利用c++11标准对局部静态变量初始化的线程安全性保证,确保首次访问时才创建实例;2. 实现简洁无需手动加锁,自动管理生命周期;3. 延迟初始化节省资源并避免初始化顺序问题;4. 相比其他方法如std::call_once、双重检查锁定或饿汉式,具有更高的安全性和简洁性且无明显性能损耗。

C++中如何实现单例模式 线程安全与延迟初始化最佳实践

在C++里,想实现一个既线程安全又能延迟创建的单例,最推荐的办法是利用C++11标准里对静态局部变量初始化的特殊保证。这基本上是现代C++中最简洁、最可靠的实现方式,它巧妙地规避了手动加锁的复杂性,同时确保了实例只在首次需要时才被创建。

C++中如何实现单例模式 线程安全与延迟初始化最佳实践

解决方案

要实现一个线程安全且延迟初始化的单例,核心思想是利用C++11及更高版本标准中对局部静态变量初始化的保证。当一个局部静态变量被声明时,它的初始化会在第一次执行到该声明语句时进行,并且C++标准保证这个初始化过程是线程安全的。这意味着即使有多个线程同时首次访问这个单例,实例也只会被创建一次,并且所有线程都能正确地获取到这个唯一的实例。

C++中如何实现单例模式 线程安全与延迟初始化最佳实践

下面是一个典型的实现:

立即学习C++免费学习笔记(深入)”;

#include 
#include 
#include 
#include 
#include  // 尽管这个方案不需要显式mutex,但为了演示其他方案可能需要

class Singleton {
public:
    // 禁用拷贝构造函数和赋值运算符,确保单例性
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 获取单例实例的唯一公共接口
    static Singleton& getInstance() {
        // C++11标准保证局部静态变量的初始化是线程安全的
        // 实例只会在第一次调用此函数时创建
        static Singleton instance;
        return instance;
    }

    void doSomething() {
        std::cout << "Singleton instance " << this << " is doing something." << std::endl;
    }

private:
    // 私有构造函数,防止外部直接创建实例
    Singleton() {
        std::cout << "Singleton instance created." << std::endl;
    }
    // 私有析构函数(可选,如果不需要特定清理逻辑,可以省略或默认)
    ~Singleton() {
        std::cout << "Singleton instance destroyed." << std::endl;
    }
};

// 示例用法
// int main() {
//     std::vector threads;
//     for (int i = 0; i < 5; ++i) {
//         threads.emplace_back([]() {
//             Singleton::getInstance().doSomething();
//         });
//     }
//
//     for (auto& t : threads) {
//         t.join();
//     }
//
//     // 验证是否是同一个实例
//     Singleton& s1 = Singleton::getInstance();
//     Singleton& s2 = Singleton::getInstance();
//     std::cout << "Address of s1: " << &s1 << std::endl;
//     std::cout << "Address of s2: " << &s2 << std::endl;
//
//     return 0;
// }

这种模式通常被称为“Meyers Singleton”或者“Magic Static Singleton”,我个人特别喜欢这种方式,因为它简直是把复杂问题简单化了,既优雅又高效。

C++中如何实现单例模式 线程安全与延迟初始化最佳实践

为什么C++11后的静态局部变量单例被认为是线程安全的最佳实践?

这其实挺有意思的,它不是我们手动加锁实现的线程安全,而是C++语言标准本身赋予的保证。在C++11之前,局部静态变量的初始化在多线程环境下可能会有问题,因为标准没有明确规定其线程安全性。但在C++11及以后的版本中,标准明确指出:如果多个线程同时试图初始化一个局部静态变量,只有一个线程会执行初始化,其他线程会阻塞,直到初始化完成。一旦初始化完成,所有线程都会获得已初始化的实例。

这背后的机制,通常由编译器和运行时库来负责实现,它们会在底层处理好必要的同步原语(比如锁),以确保这个“一次性初始化”的语义。对我们开发者来说,这就意味着我们不需要显式地去写std::mutexlock_guard或者std::call_once,代码会变得异常简洁,而且出错的可能性大大降低。它既实现了延迟初始化(因为getInstance()函数不被调用,instance就不会被创建),又保证了多线程环境下的正确性。在我看来,这简直是C++11带来的一个巨大福利,让单例模式的实现变得异常简单和健壮。

延迟初始化在单例模式中的实际意义和考量?

延迟初始化,顾名思义,就是把对象的创建推迟到它第一次被使用的时候。在单例模式里,这意味着你的单例实例不是在程序启动时就立刻被创建,而是在getInstance()函数首次被调用时才进行。这听起来可能只是个小细节,但在实际项目中,它的意义还是挺大的。

首先,它能节省资源。如果你的单例对象初始化成本很高(比如需要加载大量数据、建立网络连接或者执行复杂计算),但它又不是每次程序运行都必须立即用到,那么延迟初始化就能避免不必要的开销。程序启动会更快,内存占用也会更小,直到真正需要这个单例时才付出代价。这对于一些大型应用或者嵌入式系统来说尤其重要,启动性能和资源效率往往是关键指标。

Pi智能演示文档
Pi智能演示文档

领先的AI PPT生成工具

下载

其次,它可以避免一些复杂的初始化顺序问题。在C++中,全局或静态对象的初始化顺序有时会让人头疼,特别是当这些对象之间存在依赖关系时。如果一个全局单例在程序启动时就被初始化,而它又依赖于另一个尚未初始化的全局对象,那可能就会出问题。延迟初始化可以有效规避这类问题,因为它将单例的创建推迟到运行时,此时通常所有必要的全局资源都已经就绪。

当然,也有一些考量。比如,第一次访问单例时可能会有一个微小的延迟,因为此时需要执行初始化操作。对于对实时性要求极高的场景,这个延迟可能需要被评估。但通常情况下,这个延迟可以忽略不计,而且相比于它带来的简洁性和资源优势,这通常是个可以接受的权衡。

除了C++11静态局部变量,还有哪些实现线程安全单例的方法及其局限性?

虽然C++11的静态局部变量方案非常棒,但在某些特定场景或者为了兼容旧标准,我们可能还会遇到或需要了解其他实现线程安全单例的方法。它们各有优缺点,也体现了不同时期对线程安全问题的思考。

一种常见的替代方案是使用std::call_oncestd::once_flag。这个方法在语义上非常明确地表达了“只执行一次”的概念。它通常这样实现:

#include  // for std::once_flag and std::call_once

class SingletonWithCallOnce {
public:
    SingletonWithCallOnce(const SingletonWithCallOnce&) = delete;
    SingletonWithCallOnce& operator=(const SingletonWithCallOnce&) = delete;

    static SingletonWithCallOnce& getInstance() {
        // 确保初始化函数只被调用一次
        std::call_once(flag_, []() {
            instance_ = new SingletonWithCallOnce();
        });
        return *instance_;
    }

    void doSomething() {
        std::cout << "SingletonWithCallOnce instance " << this << " is doing something." << std::endl;
    }

private:
    SingletonWithCallOnce() {
        std::cout << "SingletonWithCallOnce instance created." << std::endl;
    }
    ~SingletonWithCallOnce() {
        std::cout << "SingletonWithCallOnce instance destroyed." << std::endl;
    }

    static SingletonWithCallOnce* instance_;
    static std::once_flag flag_;
};

// 静态成员的定义
SingletonWithCallOnce* SingletonWithCallOnce::instance_ = nullptr;
std::once_flag SingletonWithCallOnce::flag_;

std::call_once的优势在于它提供了更通用的“一次性执行”机制,你可以用它来初始化任何东西,而不仅仅是单例。但它的局限性在于,你需要手动管理instance_的生命周期,比如使用new创建后,谁来delete?如果不管,就会有内存泄漏。通常会结合智能指针(比如std::unique_ptr)来解决这个问题,但那样代码会稍微复杂一些,不如静态局部变量那么“傻瓜式”的自动管理。

还有一种历史悠久、但现在在C++11后不那么推荐的方法——“双重检查锁定”(Double-Checked Locking, DCL)。它尝试在不加锁的情况下快速判断实例是否已创建,只有在实例未创建时才加锁进行创建。

// #include 
// #include  // for std::atomic

// class SingletonDCL {
// public:
//     SingletonDCL(const SingletonDCL&) = delete;
//     SingletonDCL& operator=(const SingletonDCL&) = delete;

//     static SingletonDCL& getInstance() {
//         // 第一次检查:无锁,提高性能
//         if (instance_.load(std::memory_order_acquire) == nullptr) {
//             std::lock_guard lock(mutex_);
//             // 第二次检查:在锁内,确保只有一个线程创建实例
//             if (instance_.load(std::memory_order_relaxed) == nullptr) {
//                 instance_.store(new SingletonDCL(), std::memory_order_release);
//             }
//         }
//         return *instance_.load(std::memory_order_relaxed);
//     }

//     void doSomething() {
//         std::cout << "SingletonDCL instance " << this << " is doing something." << std::endl;
//     }

// private:
//     SingletonDCL() {
//         std::cout << "SingletonDCL instance created." << std::endl;
//     }
//     ~SingletonDCL() {
//         std::cout << "SingletonDCL instance destroyed." << std::endl;
//     }

//     static std::atomic instance_;
//     static std::mutex mutex_;
// };

// // 静态成员的定义
// std::atomic SingletonDCL::instance_ = nullptr;
// std::mutex SingletonDCL::mutex_;

说实话,这种方式在实现上容易出错,而且现代C++中已经有更简洁安全的替代方案。它需要非常精确地使用std::atomic和内存序(memory_order_acquire, memory_order_release等)来避免编译器优化和处理器重排序导致的问题。在C++11之前,DCL因为其所谓的“高性能”而备受推崇,但由于其难以正确实现,且容易出现微妙的bug,现在通常不建议直接使用它来创建单例,除非你对内存模型有非常深入的理解,并且有特殊性能需求。

最后,还有一种最简单的单例实现,就是“饿汉式”或者“Eager Initialization”。它在程序启动时就创建实例,而不是延迟创建:

// class SingletonEager {
// public:
//     SingletonEager(const SingletonEager&) = delete;
//     SingletonEager& operator=(const SingletonEager&) = delete;

//     static SingletonEager& getInstance() {
//         return instance_;
//     }

//     void doSomething() {
//         std::cout << "SingletonEager instance " << this << " is doing something." << std::endl;
//     }

// private:
//     SingletonEager() {
//         std::cout << "SingletonEager instance created." << std::endl;
//     }
//     ~SingletonEager() {
//         std::cout << "SingletonEager instance destroyed." << std::endl;
//     }

//     static SingletonEager instance_; // 在程序启动时即被创建
// };

// // 静态成员的定义 (在某个.cpp文件中)
// SingletonEager SingletonEager::instance_;

这种方法是线程安全的,因为它在main函数执行之前就被初始化了,没有任何并发问题。但它的局限性也很明显:它不是延迟初始化的。如果单例对象很大或者初始化很耗时,而你的程序在某些情况下可能根本用不到它,那么这种方式就会造成资源浪费和启动时间延长。所以,除非你明确知道单例在程序生命周期一开始就需要,否则我个人还是倾向于延迟初始化。

相关专题

更多
c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

48

2025.08.29

C++中int、float和double的区别
C++中int、float和double的区别

本专题整合了c++中int和double的区别,阅读专题下面的文章了解更多详细内容。

95

2025.10.23

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

471

2023.08.10

Python 多线程与异步编程实战
Python 多线程与异步编程实战

本专题系统讲解 Python 多线程与异步编程的核心概念与实战技巧,包括 threading 模块基础、线程同步机制、GIL 原理、asyncio 异步任务管理、协程与事件循环、任务调度与异常处理。通过实战示例,帮助学习者掌握 如何构建高性能、多任务并发的 Python 应用。

107

2025.12.24

数据库Delete用法
数据库Delete用法

数据库Delete用法:1、删除单条记录;2、删除多条记录;3、删除所有记录;4、删除特定条件的记录。更多关于数据库Delete的内容,大家可以访问下面的文章。

266

2023.11.13

drop和delete的区别
drop和delete的区别

drop和delete的区别:1、功能与用途;2、操作对象;3、可逆性;4、空间释放;5、执行速度与效率;6、与其他命令的交互;7、影响的持久性;8、语法和执行;9、触发器与约束;10、事务处理。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

207

2023.12.29

linux是嵌入式系统吗
linux是嵌入式系统吗

linux是嵌入式系统,是一种用途广泛的系统软件,其特点是:1、linux系统是完全开放、免费的;2、linux操作系统的显著优势是多用户和多任务,保证了多个用户使用互不影响;3、设备是独立的,只要安装驱动程序,任何用户都可以对任意设备进行使用和操作。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

170

2024.02.23

C++ 嵌入式系统开发入门与实践
C++ 嵌入式系统开发入门与实践

本专题将带你系统掌握 C++ 在嵌入式系统中的实战应用,内容覆盖硬件抽象、驱动开发、内存与性能优化、实时系统编程、跨平台编译构建,以及常用嵌入式框架与调试技巧,帮助开发者从零构建可运行于 MCU、ARM 等平台的高性能嵌入式项目。

185

2025.11.18

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

7

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Rust 教程
Rust 教程

共28课时 | 4万人学习

JavaScript
JavaScript

共185课时 | 15.5万人学习

HTML教程
HTML教程

共500课时 | 4.3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号