首页
社区
课程
招聘
[原创]C++ static关键字引发的思考
2023-12-9 20:57 8415

[原创]C++ static关键字引发的思考

2023-12-9 20:57
8415

基本用法

在面向对象中的用法

在类中,可以使用static关键字修饰成员函数和变量,被修饰后的函数或变量被称为静态成员函数或变量。它们属于整个类,不属于某一个对象,这意味着无需创建对象即可访问静态成员函数或变量。最常见的一个用法就是单例模式(整个类仅可只有一个对象),例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Singleton
{
public:
    static Singleton& instance() // 静态方法
    {
        static Singleton inst;  // 静态对象在instance中声明
        return inst;
    }
    int& get() { return value_; }
private:
    Singleton() : value_(0) { std::cout << "Singleton::Singleton()" << std::endl; }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    ~Singleton() { std::cout << "Singleton::~Singleton()" << std::endl; }
private:
    int value_;                 // 非静态成员变量
};
 
int value = Singleton::instance().get() // Singleton::instance()返回静态对象inst

在面向过程中的用法

此处的static主要用于限制修饰对象的作用域。

  • 静态函数,该函数仅可在当前文件中使用。
  • 静态变量,该变量仅可在当前文件中使用,而全局变量可导出给别的文件使用,虽然两者都是将变量存储在.data区域,且在程序整个声明周期都有效。
  • 静态局部变量,该变量只能被初始化一次,仅可在当前函数中使用。

新的问题

面向过程中的静态局部变量"只能被初始化一次"是如何实现的?
考虑到静态变量有可能被多次初始化且使用变量初始化,使用的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <thread>
#include <chrono>         // std::chrono::seconds
 
using namespace std;
 
void func_a();
void func_b(int a, int b);
 
int main()
{
    func_a();
    thread t1(func_b, 1, 2);
    thread t2(func_b, 3, 4);
    thread t3(func_b, 5, 6);
    thread t4(func_b, 7, 8);
 
    std::this_thread::sleep_for(std::chrono::seconds(1));
    getchar();
    t1.join();t2.join();t3.join();t4.join();
    return 0;
}
 
void func_a()
{
    static int func_a_value = 0x1234;
    cout << "func_a_value => " << func_a_value << ", current function = > " << __FUNCTION__ << endl;
}
 
void func_b(int a, int b)
{
    static int res = a + b;
    cout << "res => " << res << ", current thread id => " << std::this_thread::get_id() << endl;
}

Windows下的MSVC编译器的实现

静态局部变量func_a_value

func_a函数中是以常量初始化静态局部变量func_a_value,因此编译器直接将0x1234写入.data对应的位置,这与单(多)线程无关,与单(多)次初始化无关。
00
若函数以变量形式初始化静态局部变量,则实现初始化一次的原理见静态局部变量res

静态局部变量res

存在多个线程初始化res的情况,因此需要进行线程同步。IDA反汇编func_b函数之后的结果如下:
01
对应的C++代码如下:

1
2
3
4
5
6
7
8
9
if(pOnce > *(int *)(NtCurrentTeb()->ThreadLocalStoragePointer[0] + 0x104)  // tls中存储的全局 局部静态变量初始化计数器, 初始值为INT_MIN(0x80000000)
{
    _Init_thread_header(&pOnce)
    if ( pOnce == -1 )
    {
      res = b + a;    // 初始化代码
      _Init_thread_footer(&pOnce);
    }
}

_Init_thread_header的源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 代码所在的文件为thread_safe_statics.cpp
extern "C" void __cdecl _Init_thread_header(int* const pOnce) noexcept
{
    _Init_thread_lock();                // 进入临界区
 
    if (*pOnce == uninitialized)        // uninitialized = 0
    {
        *pOnce = being_initialized;     // being_initialized = -1
    }
    else
    {
        while (*pOnce == being_initialized)
        {
            // Timeout can be replaced with an infinite wait when XP support is
            // removed or the XP-based condition variable is sophisticated enough
            // to guarantee all waiting threads will be woken when the variable is
            // signalled.
            _Init_thread_wait(xp_timeout);
 
            if (*pOnce == uninitialized)
            {
                *pOnce = being_initialized;
                _Init_thread_unlock();
                return;
            }
        }
        _Init_thread_epoch = _Init_global_epoch; // _Init_global_epoch = INT_MIN
    }
 
    _Init_thread_unlock();              // 离开临界区
}

_Init_thread_footer的源代码如下:

1
2
3
4
5
6
7
8
9
10
// 代码所在的文件为thread_safe_statics.cpp
extern "C" void __cdecl _Init_thread_footer(int* const pOnce) noexcept
{
    _Init_thread_lock();
    ++_Init_global_epoch;
    *pOnce = _Init_global_epoch;
    _Init_thread_epoch = _Init_global_epoch;
    _Init_thread_unlock();
    _Init_thread_notify();
}

从上述代码来看,事情非常明了。pOnce指向的内存初始值为0,通过临界区来实现线程同步,同时只能有一个线程进入到_Init_thread_header和_Init_thread_footer函数。

  • CASE ONE
    若t1线程首先进入func_b,执行顺序如下:_Init_thread_header -> res = b + a -> _Init_thread_footer,此时pOnce = INT_MIN+1。
    t2线程进入func_b,此时pOnce = INT_MIN+1,既不是0也不是-1。进入_Init_thread_header之后,pOnce的值不会改变。自然而然,res的值也不会发生改变。t3和t4线程的执行情况同t2线程。
  • CASE TWO
    若res的初始化需要耗费一定的时间,比如:res = func_c(),func_c函数中调用一个sleep函数。
    那么此时的执行情况如下:
    a. t1线程,_Init_thread_header -> res的初始化流程
    b. t2线程,进入到_Init_thread_header,执行else分支中的while循环。直到t1线程的res初始化过程结束,然后调用_Init_thread_footer修改pOnce的值,pOnce的值变为INT_MIN+1。t2线程会退出while循环,pOnce值不为-1,自然也就不会再次初始化。
    c. 若t3或t4线程同t2线程,也执行func_b函数,则会被_Init_thread_lock函数阻塞。若t3或t4在t1执行_Init_thread_footer函数后调用func_b,这种情况同CASE ONE下。

因为线程是乱序执行的,所以res的结果不是确定的,如下图:
02
02

Linux下的G++编译器的实现

源码的编译环境是Ubuntu 22.04,g++ 11.4.0

静态局部变量func_a_value

同Windows下的常量赋值给静态局部变量,IDA的视图如下:
04
变量赋值给静态局部变量见"静态局部变量res"章节

静态局部变量res

IDA反汇编func_b的结果如下:
05
从该图中可以看出,__cxa_guard_acquire发挥同_Init_thread_header相同的效果,而__cxa_guard_release发挥同_Init_thread_footer相同的效果。这两个函数的源码如下:
__cxa_guard_acquire函数源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// guard.cc
int __cxa_guard_acquire (__guard *g)            // typedef int __guard, 初始值为0
{
    if (_GLIBCXX_GUARD_TEST_AND_ACQUIRE (g))    //
      return 0;
 
    if (__gnu_cxx::__is_single_threaded())      // 调用pthread_create时,__gnu_cxx::__is_single_threaded() 为false
    {
    // No need to use atomics, and no need to wait for other threads.
        int *gi = (int *) (void *) g;
        if (*gi == 0)
        {
            *gi = _GLIBCXX_GUARD_PENDING_BIT;   // 0x100
            return 1;
        }
        else
        {
            throw_recursive_init_exception();
        }
    }
    else
    {
        int *gi = (int *) (void *) g;
        const int guard_bit = _GLIBCXX_GUARD_BIT;               // 1
        const int pending_bit = _GLIBCXX_GUARD_PENDING_BIT;     // 0x100
        const int waiting_bit = _GLIBCXX_GUARD_WAITING_BIT;     // 0x10000
 
        while (1)
        {
            int expected(0);
            if (__atomic_compare_exchange_n(gi, &expected, pending_bit, false,
                        __ATOMIC_ACQ_REL,
                        __ATOMIC_ACQUIRE))
            {
                return 1;   // This thread should do the initialization.
            }
           
            if (expected == guard_bit)
            {
                // Already initialized.
                return 0;  
            }
 
            if (expected == pending_bit)
            {
                // Use acquire here.
                int newv = expected | waiting_bit;    // 0x10100
                if (!__atomic_compare_exchange_n(gi, &expected, newv, false,
                          __ATOMIC_ACQ_REL,
                          __ATOMIC_ACQUIRE))
                {
                    if (expected == guard_bit)      // 1
                    {
                        // Make a thread that failed to set the
                        // waiting bit exit the function earlier,
                        // if it detects that another thread has
                        // successfully finished initialising.
                        return 0;
                    }
                    if (expected == 0)
                        continue;
                }
          
                expected = newv;
            }
 
            syscall (SYS_futex, gi, _GLIBCXX_FUTEX_WAIT, expected, 0);
        }
    }
   return acquire (g);
}

__cxa_guard_release函数源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// guard.cc
extern "C" void __cxa_guard_release (__guard *g) noexcept
{
    // If __atomic_* and futex syscall are supported, don't use any global
    // mutex.
    if (__gnu_cxx::__is_single_threaded())
    {
        int *gi = (int *) (void *) g;
        *gi = _GLIBCXX_GUARD_BIT;       // 1
        return;
    }
    else
    {
        int *gi = (int *) (void *) g;
        const int guard_bit = _GLIBCXX_GUARD_BIT;
        const int waiting_bit = _GLIBCXX_GUARD_WAITING_BIT;
        int old = __atomic_exchange_n (gi, guard_bit, __ATOMIC_ACQ_REL);
 
        if ((old & waiting_bit) != 0)
        syscall (SYS_futex, gi, _GLIBCXX_FUTEX_WAKE, INT_MAX);
        return;
    }
    set_init_in_progress_flag(g, 0);
    _GLIBCXX_GUARD_SET_AND_RELEASE (g);
}

像Windows MSVC平台那样分CASE分析,初始状态*g = 0,__gnu_cxx::__is_single_threaded() = true。__gnu_cxx::__is_single_threaded()在pthread_create函数已经被设置为了false,这个操作在执行func_b函数之前。

  • CASE ONE
    a. 若t1线程首先进入func_b,执行顺序如下:__cxa_guard_acquire-> res = b + a -> __cxa_guard_release,此时g = 0x1。
    b. t2 线程进入时,因为
    g = 0x1,t2在__cxa_guard_acquire函数的41行返回,因返回值为0,因此不会对res再次初始化。t3和t4所遇情况,同t2线程。
  • CASE TWO
    res的初始化需要较长时间。此时的情况如下:
    a. t1线程,__cxa_guard_acquire-> res的初始化流程,此时g = 0x100。
    b. t2线程进入到__cxa_guard_acquire函数中,会执行syscall系统调用进行阻塞,此时
    g = 0x10100。直到在t1调用__cxa_guard_release解除t2线程的阻塞,*g = 1。
    c. 若t3或t4线程同t2线程,也执行func_b函数,则会被syscall(SYS_futex)系统调用阻塞。若t3或t4在t1执行__cxa_guard_release函数后调用func_b,这种情况同CASE ONE下。

总结

  • 使用常量初始化静态局部变量,MSVC和G++实现的方法相同
  • 使用变量初始化静态局部变量,包括:多线程和单线程,CASE ONE和CASE TWO各个线程面对情况是一样的,只不过是两种平台的同步机制不同而已。

[培训]内核驱动高级班,冲击BAT一流互联网大厂工 作,每周日13:00-18:00直播授课

最后于 2023-12-9 22:04 被baolongshou编辑 ,原因:
上传的附件:
收藏
点赞7
打赏
分享
最新回复 (2)
雪    币: 4974
活跃值: (3723)
能力值: ( LV13,RANK:270 )
在线值:
发帖
回帖
粉丝
baolongshou 2 2023-12-9 21:32
2
0
mac os 平台留给观众自己分析一下了
雪    币: 19785
活跃值: (29397)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-12-9 23:33
3
1
感谢分享
游客
登录 | 注册 方可回帖
返回