【问题标题】:helgrind reports possible race between use of singleton and its constructorhelgrind 报告了单例的使用和它的构造函数之间可能存在的竞争
【发布时间】:2017-06-23 04:14:05
【问题描述】:

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 的读取和设置value 的ctor 之间的可能数据竞争。

==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 报告可能的竞赛?

【问题讨论】:

  • 如果你在初始化过程中得到汇编程序,它看起来好像正在使用锁吗?
  • 读取local_key.value 周围肯定没有任何锁定。至于初始化,是的,它被__cxa_guard_acquire__cxa_guard_release括起来

标签: c++ thread-safety valgrind


【解决方案1】:

反汇编函数thread1,我看到对__cxa_guard_acquire的调用 和 __cxa_guard_release,我认为我们可以合理地假设是 保护构造函数。但是,此类呼叫不会被拦截 通过 helgrind,因此,helgrind 没有观察到任何同步。这是 Valgrind/helgrind 中的一个错误/弱点,值得在 valgrind bugzilla 上提交一个错误。但是请注意,快速阅读代码,对 __cxa_guard_acquire 和 __cxa_guard_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 工具同样存在相同的错误/弱点。

【讨论】:

  • 我认为你是对的。 __cxa_guard_acquire__cxa_guard_release there 上有文档。如果_acquire 告诉它初始化已经完成,代码会跳过_release。这可能是 helmgrind 尽力而为的典型情况,但无法判断没有种族。我会考虑这个,也许会报告。
  • 参见this threadvalgrind-users
【解决方案2】:

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

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-01
    相关资源
    最近更新 更多