Helgrind 报告了使用 Singleton 及其构造函数之间可能存在的竞争

helgrind reports possible race between use of singleton and its constructor

本文关键字:之间 存在 竞争 构造函数 报告 Singleton Helgrind      更新时间:2023-10-16

如前所述,Meyer 的单例在 C++11 中是线程安全的。

所以我希望这段代码很好:

#include <stdio.h>
#include <pthread.h>
struct key_type {
int value;
key_type() : value(0) { }
};  

void * thread1(void*) {
static key_type local_key;
printf("thread has key %dn", local_key.value);
return NULL;
}   
int main()
{
pthread_t t[2];
pthread_create(&t[0], NULL, thread1, NULL);
pthread_create(&t[1], NULL, thread1, NULL);
pthread_join(t[0], NULL);
pthread_join(t[1], NULL);
}   

(故意过度简化代码,我知道我可以简单地进行零初始化。

我正在使用 g++-7.1.0 进行编译。Helgrind (valgrind-3.12.0) 报告了读取local_key.value和 ctor 之间可能存在的数据竞争,这设置了value

==29036== Possible data race during read of size 4 at 0x601058 by thread #3
==29036== Locks held: none
==29036==    at 0x4006EA: thread1(void*) (datarace-simplest.cpp:12)
==29036==    by 0x4C32D06: mythread_wrapper (hg_intercepts.c:389)
==29036==    by 0x4E45493: start_thread (pthread_create.c:333)
==29036==    by 0x59DEAFE: clone (clone.S:97)
==29036== 
==29036== This conflicts with a previous write of size 4 by thread #2
==29036== Locks held: none
==29036==    at 0x400780: key_type::key_type() (datarace-simplest.cpp:6)
==29036==    by 0x4006DF: thread1(void*) (datarace-simplest.cpp:11)
==29036==    by 0x4C32D06: mythread_wrapper (hg_intercepts.c:389)
==29036==    by 0x4E45493: start_thread (pthread_create.c:333)
==29036==    by 0x59DEAFE: clone (clone.S:97)
==29036==  Address 0x601058 is 0 bytes inside data symbol "_ZZ7thread1PvE9local_key"

我认为 c++11 标准 (§6.7) 保证local_key被初始化一劳永逸,以便进一步的访问处理其 ctor 保证不会仍在运行的变量。

否则,第一次初始化这样的变量 控制通过其声明;考虑这样的变量 初始化完成后初始化。[...]如果控件同时输入声明,而 变量正在初始化,并发执行应等待 以完成初始化。[...]

我错了吗?是赫尔格林德缺陷吗?是否已知此用例会从裂缝中溜走,以便赫尔格林德报告可能的比赛?

反汇编函数线程 1,我看到对__cxa_guard_acquire的调用 和__cxa_guard_release,我认为我们可以合理地假设是什么 保护构造函数。但是,此类调用不会被拦截 赫尔格林德,因此,赫尔格林德没有观察到任何同步。这是Valgrind/helgrind中的一个错误/弱点,值得在valgrind bugzilla上提交一个错误。但请注意,快速读取代码,对 __cxa_guard_acquire 和 __cxa_guard_release 的调用似乎与一对锁定/解锁并不直接匹配:看起来代码可能只调用 acquire,然后不调用释放:

00x000000000040077e <+24>:   mov    $0x600d00,%edi
0x0000000000400783 <+29>:    callq  0x400610 <__cxa_guard_acquire@plt>
0x0000000000400788 <+34>:    test   %eax,%eax
0x000000000040078a <+36>:    setne  %al
0x000000000040078d <+39>:    test   %al,%al
0x000000000040078f <+41>:    je     0x4007a5 <thread1(void*)+63>
0x0000000000400791 <+43>:    mov    $0x600d08,%edi
0x0000000000400796 <+48>:    callq  0x40082e <key_type::key_type()>
0x000000000040079b <+53>:    mov    $0x600d00,%edi
0x00000000004007a0 <+58>:    callq  0x400650 <__cxa_guard_release@plt>
0x00000000004007a5 <+63>:    mov    0x20055d(%rip),%eax        # 0x600d08 <_ZZ7thread1PvE9local_key>

调试了一下后,看起来守卫就在前面 local_key,并在构造对象后设置为 1。 我不太清楚__cxa_guard_release必须做什么。我想需要对 c++ 运行时库代码进行更多阅读,以了解 helgrind 如何(也许)被指示那里发生了什么。

另请注意,valgrind drd 工具同样存在相同的错误/弱点。

我认为这是一个地狱般的缺陷。该标准保证静态初始化在稍后读取之前进行排序,并且证据(见下文)表明,不仅决定是否运行构造函数,而且实际上整个构造函数都在锁后面。

修改示例,使构造函数读取

key_type() : value(0) {
sleep (1);
pthread_yield();
value = 42;
}

输出 42 两次。这表明在测试(如有必要)和从初始化开始时获取的锁在构造函数完成之前不会释放。

除了现有的答案之外,如果您查看生成的完整程序集,您会发现在锁定之前对保护变量进行了额外的检查。 对此(锁定下)的写入与对此的读取(锁定外的"预览")可能被视为数据竞赛。

看这里: https://godbolt.org/z/3bf66qr4G

getInstance():
pushq   %rbp
movq    %rsp, %rbp

# this is a "preview", probably seen as a data race:
movzbl  guard variable for getInstance()::inst(%rip), %eax
testb   %al, %al
sete    %al
testb   %al, %al
je      .L4

# preview says "does not exist"
# check again whether instance exists, under lock
movl    $guard variable for getInstance()::inst, %edi
call    __cxa_guard_acquire
testl   %eax, %eax
setne   %al
testb   %al, %al
je      .L4

# instance does not exist - create one
movl    $_ZZ11getInstancevE4inst, %edi
call    Foo::Foo() [complete object constructor]
movl    $__dso_handle, %edx
movl    $_ZZ11getInstancevE4inst, %esi
movl    $_ZN3FooD1Ev, %edi
call    __cxa_atexit
movl    $guard variable for getInstance()::inst, %edi
call    __cxa_guard_release

.L4:
movl    $_ZZ11getInstancevE4inst, %eax
popq    %rbp
ret