helgrind报告使用singleton及其构造函数之间可能的竞争

时间:2017-06-22 11:41:33

标签: c++ thread-safety valgrind

如上所述there,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 %d\n", 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之间报告可能的数据竞赛,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保证不会继续运行的变量。

  

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

我错了吗?这是一个helgrind缺陷吗?这个用例是否已知漏洞,以便helgrind报告可能的种族?

2 个答案:

答案 0 :(得分:0)

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

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

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

outputs 42两次。这表明在构造函数完成之前,在必要时进行测试并从初始化开始获取的锁定不会被释放。

答案 1 :(得分:0)

反汇编函数thread1,我看到对__cxa_guard_acquire的调用 和__cxa_guard_release,我认为我们可以合理地假设是什么 保护构造函数。但是,这些电话不会被截获 通过helgrind,所以,helgrind没有观察到任何同步。这是Valgrind / helgrind的错误/弱点,值得在valgrind bugzilla上提交一个错误。但请注意,快速读取代码时,对__cxa_guard_acquire和__cxa_guard_release的调用似乎与一对锁定/解锁不匹配:看起来代码可能只调用acquire,然后不调用release:

   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工具同样会遇到相同的bug /弱点。