几种线程安全的初始化方式

文摘   2025-01-02 12:06   新加坡  

 



你好,我是雨乐~


在多线程编程场景中,如果多个线程对同一块数据区域进行读写操作,我们称之为这种存在Data Race。在这种场景下,第一反应是使用std::mutex来保证,如果一个变量在初始化之后从未被修改,那么就不需要使用昂贵的锁或者原子操作进行同步。我们只需要确保它在初始化时是线程安全的即可。

今天,聊聊几个常用的线程安全的初始化方式。

constexpr

常量表达式constexpr)是指在编译时就能确定其值的表达式,因此它们天生是线程安全的。另一方面,常量表达式必须在声明时立即初始化,这就进一步保证了其线程安全。

比如像下面这样定义:

constexpr pi = 3.14;

当然了,很多时候,这种元类型不一定能满足我们的需求,比如需要定义成自定义类型或者使用STL的结构。

犹记得首次接触constexpr的时候,将std::map声明为constexpr,然后报了如下这种编译错误:

 error: the type 'const std::map<int, int>' of 'constexpr' variable 'mii' is not literal
  187 |     constexpr std::map<intint> mii;
      |                                  ^~~
In file included from /opt/compiler-explorer/gcc-14.2.0/include/c++/14.2.0/map:63,
                 from <source>:7:
/opt/compiler-explorer/gcc-14.2.0/include/c++/14.2.0/bits/stl_map.h:102:11: note: 'std::map<int, int>' is not literal because:
  102 |     class map
      |           ^~~
/opt/compiler-explorer/gcc-14.2.0/include/c++/14.2.0/bits/stl_map.h:102:11: note:   'std::map<int, int>' does not have 'constexpr' destructor

经过调研后才发现constexpr对其数据类型有如下限制:

1、不能有虚函数或虚基类

  • • 如果一个类包含 虚成员函数,或者类的基类是 虚基类,那么该类就不能被用于常量表达式。这是因为虚函数涉及到运行时的动态绑定,而常量表达式的计算必须发生在编译时,无法处理这种运行时的行为

2、构造函数必须是常量表达式

  • • 该类的 构造函数 必须是常量表达式(即编译时可以求值的函数)。这样,在编译时就可以正确地创建该类的实例,而不依赖运行时的计算

3、每个基类和非静态成员必须被初始化

  • • 该类的每个 基类 和 非静态成员 都必须在编译时初始化。这意味着,在常量表达式中,所有的成员都必须有明确的初始值,不能存在未初始化的成员

4、成员函数必须是常量表达式

  • • 类的成员函数,如果要在常量表达式中调用,也必须是常量表达式。也就是说,这些成员函数本身必须是可以在编译时被执行的,不能包含任何需要运行时求值的操作

基于以上要求,一个简单的支持constexpr的类如下:

#include <iostream>

classObj {
public:
constexpr Obj(int32_t num) : num_(num) {}
constexpr int32_t Get() const return num_; }
private:
int32_t num_;
};

int main() {
constexpr Obj obj{1};
constexprint32_t num = obj.Get();

return0;
}

std::call_once & std::once_flag

在很多情况下,我们不能保证都可以使用constexpr来实现初始化,基于此,C++提供了另外一种方式可以保证在多线程,即结合使用std::call_once & std::once_flag

std::call_once 和 std::once_flag 是 C++ 中用于保证某个函数只会执行一次的机制,尤其是在多线程环境下。std::call_once 用于确保一个可调用对象(如函数、lambda 表达式等)只会被执行一次。std::once_flag 是一个标志,用来指示一个操作是否已经执行过。它通常与 std::call_once 一起使用。其主要作用是标记一个操作是否已经成功执行,确保同一个操作(函数)不会在多线程环境中被重复执行。

我们看下如下这个例子:

#include <iostream>
#include <thread>
#include <mutex>

std::once_flag flag;

void init() {
    std::cout << "Initialization function is called!" << std::endl;
}

void thread_function() {
    std::call_once(flag, init);
}

int main() {
    std::thread t1(thread_function);
    std::thread t2(thread_function);
    std::thread t3(thread_function);

    t1.join();
    t2.join();
    t3.join();

    return0;
}

显然,只会输出一次Initialization function is called!

另一个比较有名的示例就是单例模式,同样,我们可以使用std::call_once & std::once_flag来达到在多线程环境下只会初始化一次。

#include <iostream>
#include <mutex>
#include <thread>

// 单例类
classSingleton {
public:
    // 获取单例实例
    static Singleton& getInstance() {
        std::call_once(flag, []() {
            instance.reset(newSingleton());  // 只会被调用一次
        });
        return *instance;
    }

    void showMessage() {
        std::cout << "Singleton instance is working!" << std::endl;
    }

    Singleton() { std::cout << "Singleton initialized!" << std::endl; }
    ~Singleton() { std::cout << "Singleton destroyed!" << std::endl; }
private:
   inlinestatic std::unique_ptr<Singleton> instance;  // 静态成员,存储单例实例
   inlinestatic std::once_flag flag;  // 标记单例初始化是否完成
};

void threadFunction() {
    Singleton& s = Singleton::getInstance();
    s.showMessage();
}

int main() {
    std::thread t1(threadFunction);
    std::thread t2(threadFunction);
    std::thread t3(threadFunction);

    t1.join();
    t2.join();
    t3.join();

    return0;
}

static

既然在前面提到了单例模式,那么就不得不提到Meyers Singleton,由 Scott Meyers 提出的,在其著作《Effective C++》中介绍的单例模式的实现方式,采用了“懒初始化”策略,即在第一次使用单例实例时才创建实例,而不是在程序启动时就创建。

可能你会有所疑问,本文的目的不是线程安全的初始化么?怎么突然提到了单例模式?

其实,单例模式,在多线程环境下,核心点就要做到线程安全,这就要求在多线程下,只能被实例化一次,而Meyers Singleton完全满足了该条件,不过Meyers Singleton的前提是C++11及以上。

#include <iostream>

classSingleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;  // 线程安全的初始化
        return instance;
    }

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

    void showMessage() const {
        std::cout << "Hello from Singleton!" << std::endl;
    }

private:
    Singleton() {
        std::cout << "Singleton created!" << std::endl;
    }
    ~Singleton() {
        std::cout << "Singleton destroyed!" << std::endl;
    }
};

int main() {
    Singleton& s1 = Singleton::getInstance();
    s1.showMessage();

    Singleton& s2 = Singleton::getInstance();
    s2.showMessage();

    return0;
}

 

下期见~

以上

如果对本文有疑问可以加笔者微信直接交流,笔者也建了C/C++相关的技术群,有兴趣的可以联系笔者加群。

推荐阅读  点击标题可跳转

1、聊聊并发编程的使用场景

2、聊聊并发编程中的这个新特性

3、再见了!atomic!!


雨乐聊编程
毕业于中国科学技术大学,现任某互联网公司高级技术专家一职。本公众号专注于技术交流,分享日常有意思的技术点、线上问题等,欢迎关注