本地静态指针变量不是线程安全的

Local static pointer variable is not thread safe?

本文关键字:线程 安全 变量 静态 指针      更新时间:2023-10-16

在我的服务器模块上,有时log4cxx库会使它崩溃。

这是因为。。。

LevelPtr Level::getTrace() {
   static LevelPtr level(new Level(Level::TRACE_INT, LOG4CXX_STR("TRACE"), 7));
   return level;
}

static LevelPtr返回null ptr。

我测试了以下代码。

int start_flag = 0;
class test_dummy {
public:
    int mi;
    test_dummy() : mi(1)
    { 
        std::cout << "hey!n"; 
    }
    static test_dummy* get_p() 
    { 
        static test_dummy* _p = new test_dummy();
        return _p;
    }
};
void thread_proc()
{
    int i = 0;
    while (start_flag == 0)
    {
        i++;
    }
    if (test_dummy::get_p() == 0)
    {
        std::cout << "error!!!n";
    }
    else
    {
        std::cout << "mi:" << test_dummy::get_p()->mi << "n";
    }
}
void main()
{
    boost::thread *pth_array[5] = {0,};
    for (int i = 0; i < 5; i++)
    {
        pth_array[i] = new boost::thread(thread_proc);
    }
    start_flag = 1;
    for (int i = 0; i < 5; i++)
    {
        pth_array[i]->join();
    }
    std::cin.ignore();
}

这确实是线程不安全的,但我很好奇为什么get_p()返回空指针而不是另一个分配的地址。

这是因为在执行new()操作时,值被设置为0吗?

编译器提供的这段代码中有一个竞争条件:

if (!level_initialized)
{
    level_initialized = 1;
    level = new Level(...);
}
return level; 

在clang++3.5中,似乎有锁可以防止这种竞争,但如果不仔细查看编译器生成的代码,就不可能确切地说出发生了什么。但我怀疑这就是发生的事情。

以下是clang++3.5生成的(减去一些杂波)

_Z8getTracev:                           # @_Z8getTracev
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $32, %rsp
    cmpb    $0, _ZGVZ8getTracevE5level       # Guard variable
    jne .LBB0_4
    leaq    _ZGVZ8getTracevE5level, %rdi
    callq   __cxa_guard_acquire
    cmpl    $0, %eax
    je  .LBB0_4
.Ltmp0:
    movl    $4, %eax
    movl    %eax, %edi
    callq   _Znwm                            # new
.Ltmp1:
    movq    %rax, -24(%rbp)         # 8-byte Spill
    jmp .LBB0_3
.LBB0_3:                                # %invoke.cont
    leaq    _ZGVZ8getTracevE5level, %rdi
    movq    -24(%rbp), %rax         # 8-byte Reload
    movq    -24(%rbp), %rcx         # 8-byte Reload
    movl    $0, (%rcx)
    movq    %rax, _ZZ8getTracevE5level
    callq   __cxa_guard_release
.LBB0_4:                                # %init.end
    movq    _ZZ8getTracevE5level, %rax
    addq    $32, %rsp
    popq    %rbp
    retq

我修改了代码,将Level用作int,等等,所以它比您从发布的代码中获得的代码更简单。

很难说太多,因为代码显然没有定义行为,但标准确实要求level在调用CCD_ 4之前初始化为空指针。在以确保本地静态被准确初始化一次,编译器或多或少地必须添加一个额外的标志;类似于:

static test_dummy* _p = nullptr;
static bool isInitialized = false;
if ( !isInitialized ) {
    _p = new test_dummy();
    isInitialized = true;
}

(当然,事实上,上面显示的初始化是零初始化,它发生在其他任何事情之前。和聪明的编译器可以实现显式初始化第一次发生到不能导致CCD_ 5空指针,并使用_p作为控制变量。)

以上内容不安全;为了使其线程安全,必须保护整个序列。(还有更多或不那么复杂的技巧来避免对完全互斥的需要,但是在所有情况下,isInitialized的所有访问都必须是原子访问。)

如果序列不受保护,则另一个线程看到的顺序写入未定义。所以一些线程正在看到isInitialized为true,但仍在中看到空指针_p