【C/C++】探索单例模式:线程安全与性能优化

Singleton

1 指针版本

Version 1 非线程安全版本

class Logger {
public:
    static Logger *GetInstance() {
        if (instance == nullptr) {
            instance = new Logger();
        }

        return instance;
    }

    void Log(const std::string &message) {
        std::cout << message << std::endl;
    }

private:
    static Logger *instance;

    Logger() {}
};

Logger *Logger::instance = nullptr;

Version 2 加锁版本

增加锁,用于保证线程安全,但是锁开销会影响性能。

class Logger {
public:
    static Logger *GetInstance() {
        std::lock_guard<std::mutex> lk(mutex_);

        if (instance == nullptr) {
            instance = new Logger();
        }

        return instance;
    }

    void Log(const std::string &message) {
        std::cout << message << std::endl;
    }

private:
    Logger() {}

    static Logger *instance;
    static std::mutex mutex_;
};

Logger *Logger::instance = nullptr;
std::mutex Logger::mutex_;

Version 3.1 双重检查锁版本 Atomic+Mutex

class Logger {
public:
    static Logger* GetInstance() {
        // First, attempt to load the current instance atomically
        Logger* tmp = instance.load(std::memory_order_acquire);
        
        // If the instance is nullptr, create it
        if (tmp == nullptr) {
            std::lock_guard<std::mutex> lock(mtx);  // Lock only during initialization
            
            tmp = instance.load(std::memory_order_relaxed);  // Check again inside the lock
            
            if (tmp == nullptr) {
                tmp = new Logger();  // Create a new instance
                instance.store(tmp, std::memory_order_release);  // Atomically set the instance
            }
        }

        return tmp;
    }

    void Log(const std::string& message) {
        std::cout << message << std::endl;
    }

    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

private:
    Logger() {}  // Private constructor to prevent direct instantiation

    static std::atomic<Logger*> instance;  // Atomic pointer to the Singleton instance
    static std::mutex mtx;  // Mutex to protect initialization
};

// Initialize the atomic pointer and mutex
std::atomic<Logger*> Logger::instance(nullptr);
std::mutex Logger::mtx;

Version 3.2 双重检查锁版本 Atomic-only

class Logger {
public:
    static Logger* GetInstance() {
        // First, attempt to load the current instance atomically
        Logger* tmp = instance.load(std::memory_order_acquire);
        
        // If the instance is nullptr, create it
        if (tmp == nullptr) {
            tmp = new Logger();  // Create a new instance
            
            // Atomically set the instance if no other thread has done so
            if (!instance.compare_exchange_strong(tmp, tmp)) {
                delete tmp; // Another thread won the race, delete the temporary instance
                tmp = instance.load(std::memory_order_acquire); // Reload the instance
            }
        }

        return tmp;
    }

    void Log(const std::string& message) {
        std::cout << message << std::endl;
    }

    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

private:
    Logger() {}  // Private constructor to prevent direct instantiation

    static std::atomic<Logger*> instance;  // Atomic pointer to the Singleton instance
};

// Initialize the atomic pointer to nullptr
std::atomic<Logger*> Logger::instance(nullptr);

Version 3 两种方式对比

  • Only Atomic:

    • Atomic Check: We first check the instance atomically with instance.load. If it’s nullptr, we attempt to create the instance using new.
    • Atomic Set: We use compare_exchange_strong to ensure that only one thread creates the instance. If another thread has already created the instance, it returns the existing one.
    • No Mutex: There is no mutex involved here. The atomic operations ensure thread safety during the initialization phase.
  • Atomic + Mutex:

    • Atomic First Check: The first check of the instance pointer is atomic using instance.load.
    • Mutex Locking: If the instance is nullptr, we lock a mutex (std::mutex mtx) to synchronize access during the actual creation of the instance.
    • Double Check Inside Lock: After acquiring the mutex, we perform another check of the instance. This prevents other threads from creating multiple instances if they were waiting on the mutex.
    • Atomic Set: We use instance.store to atomically set the instance pointer once it’s initialized.

  • Comparison of Effectiveness:
FactorAtomic-onlyAtomic + Mutex
InitializationAtomic operations ensure safe initialization.Mutex ensures exclusive access during initialization.
Post-Initialization AccessLock-free after initialization.Mutex locking still required to access instance.
Performance (High Concurrency)High performance: No lock contention after init.Slower due to mutex locking, even after initialization.
Scalability (Concurrency)Highly scalable: No locks post-initialization.Less scalable: Mutex lock can cause contention.
Memory ConsistencyEnsured via atomic operations and memory_order_acquire/release.Ensured by std::mutex for synchronization.
SimplicitySlightly more complex due to atomic operations.Simpler for developers familiar with mutexes.
  • Atomic-only approach is more effective in high-concurrency environments, especially when you expect many threads accessing the Singleton. Since the initialization is thread-safe and lock-free after the instance is created, it scales much better than the mutex-based approach.

  • Atomic + Mutex approach might be easier to understand for developers familiar with mutexes and might work well in lower-concurrency environments. However, the mutex adds overhead for each access, and if the program has many threads, it will result in contention and slower performance.

  • If you are building a highly concurrent system, prefer the atomic-only approach, as it will perform better with minimal locking overhead.

  • If you have a simpler, lower-concurrency application, using atomic + mutex might be a good trade-off because it provides simplicity and guarantees correct initialization with easy-to-understand synchronization.

2 引用版本

Version 1 简单版本 不推荐

class Logger {
public:
    static Logger &GetInstance() {
        return instance;
    }

    void Log(const std::string &message) {
        std::cout << message << std::endl;
    }
    
private:
    static Logger instance;
    Logger() {
    }
};

Logger Logger::instance;

Version 2 初始化安全版本

c++机制保证初始化安全

class Logger {
public:
    static Logger& GetInstance() {
        static Logger instance;

        return instance;
    }

    void Log(const std::string &message) {
        std::cout << message << std::endl;
    }

    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

private:
    Logger() {}
};

Version 3 初始化+操作安全版本

增加操作安全

class Logger {
public:
    static Logger &GetInstance() {
        static Logger instance;

        return instance;
    }

    void Log(const std::string &message) {
        std::lock_guard<std::mutex> lk(mtx);

        std::cout << message << std::endl;
    }


    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

private:
    Logger() {}
    std::mutex mtx;
};

Explanation

初始化过程线程安全原因:

  1. Static Local Variable:

    • In the GetInstance() method, we declare a static local variable instance.

      • static Logger instance; ensures that instance is only created once and persists for the entire lifetime of the program.
  2. First-Time Initialization:

    • The first time GetInstance() is called, the static variable instance is initialized. This is where the thread-safety comes into play. The C++11 standard guarantees that the initialization of a static local variable will be thread-safe.
    • If multiple threads try to call GetInstance() simultaneously, only one thread will initialize the instance. The other threads will wait until the initialization is complete, and then they will all see the same instance when they call GetInstance() again.
  3. Thread-Safe Static Initialization:

    • The C++11 guarantee ensures that even if multiple threads try to initialize the instance simultaneously, the static variable will only be initialized once. The other threads will see the already initialized object, which eliminates any race condition.
  4. Post-Initialization Access:

    • After initialization, the reference instance is ready for access, and since it is a static variable, it is always available. There is no locking required for accessing instance after it is initialized, making access very efficient.
  5. No Mutex or Atomic Operations:

    • Since the C++ standard guarantees thread-safe initialization of static local variables, there is no need for additional synchronization mechanisms such as mutexes or atomic operations. The instance is initialized only once, and once it is initialized, it is ready for fast, lock-free access.

Comparison

FactorAtomic-only SingletonAtomic + Mutex SingletonReference Singleton
Thread-Safe InitializationThread-safe initialization using atomic operations.Thread-safe initialization using atomic + mutex locking.Guaranteed thread-safe initialization due to static storage duration in C++11.
Memory ManagementRequires dynamic memory allocation (using new).Requires dynamic memory allocation (using new).No dynamic memory allocation; the instance is static.
Post-Initialization AccessLock-free after initialization, very fast.Mutex still required for each access.Lock-free after initialization, very fast.
Performance (High Concurrency)Very high performance due to lock-free access.Lower performance due to mutex lock overhead.Very high performance with no locking or atomic ops.
Scalability (Concurrency)Highly scalable with minimal contention.Less scalable due to mutex contention.Highly scalable since there’s no contention.
SimplicityMore complex, requires understanding of atomic operations.More complex due to mutex usage and atomic operations.Simpler and more straightforward.
Memory UsageRequires dynamic memory allocation for the Singleton.Requires dynamic memory allocation for the Singleton.No dynamic memory allocation, very efficient.
Lifetime ManagementRequires manual cleanup or reliance on smart pointers.Requires manual cleanup or reliance on smart pointers.Managed automatically by the compiler with static duration.
SafetyThread-safe, but requires careful handling of atomic ops.Thread-safe, but introduces locking overhead.Thread-safe due to the C++ static initialization guarantee, no locking needed.
Use CaseSuitable for high-concurrency, dynamic memory applications where you need to fine-tune memory allocation.Suitable for high-concurrency, but mutex introduces some overhead in high-load
### LlamaIndex 多模态 RAG 实现 LlamaIndex 支持多种数据类型的接入与处理,这使得它成为构建多模态检索增强生成(RAG)系统的理想选择[^1]。为了实现这一目标,LlamaIndex 结合了不同种类的数据连接器、索引机制以及强大的查询引擎。 #### 数据连接器支持多样化输入源 对于多模态数据的支持始于数据收集阶段。LlamaIndex 的数据连接器可以从多个异构资源中提取信息,包括但不限于APIs、PDF文档、SQL数据库等。这意味着无论是文本还是多媒体文件中的内容都可以被纳入到后续的分析流程之中。 #### 统一化的中间表示形式 一旦获取到了原始资料之后,下一步就是创建统一而高效的内部表达方式——即所谓的“中间表示”。这种转换不仅简化了下游任务的操作难度,同时也提高了整个系统的性能表现。尤其当面对复杂场景下的混合型数据集时,良好的设计尤为关键。 #### 查询引擎助力跨媒体理解能力 借助于内置的强大搜索引擎组件,用户可以通过自然语言提问的形式轻松获得所需答案;而对于更复杂的交互需求,则提供了专门定制版聊天机器人服务作为补充选项之一。更重要的是,在这里实现了真正的语义级关联匹配逻辑,从而让计算机具备了一定程度上的‘认知’功能去理解和回应人类意图背后所蕴含的意义所在。 #### 应用实例展示 考虑到实际应用场景的需求多样性,下面给出一段Python代码示例来说明如何利用LlamaIndex搭建一个多模态RAG系统: ```python from llama_index import GPTSimpleVectorIndex, SimpleDirectoryReader, LLMPredictor, PromptHelper, ServiceContext from langchain.llms.base import BaseLLM import os def create_multi_modal_rag_system(): documents = SimpleDirectoryReader(input_dir='./data').load_data() llm_predictor = LLMPredictor(llm=BaseLLM()) # 假设已经定义好了具体的大型预训练模型 service_context = ServiceContext.from_defaults( chunk_size_limit=None, prompt_helper=PromptHelper(max_input_size=-1), llm_predictor=llm_predictor ) index = GPTSimpleVectorIndex(documents, service_context=service_context) query_engine = index.as_query_engine(similarity_top_k=2) response = query_engine.query("请描述一下图片里的人物表情特征") print(response) ``` 此段脚本展示了从加载本地目录下各类格式文件开始直到最终完成一次基于相似度排序后的top-k条目返回全过程。值得注意的是,“query”方法接收字符串参数代表使用者想要询问的内容,而在后台则会自动调用相应的解析模块并结合先前准备好的知识库来进行推理计算得出结论。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值