C ++多线程:本地静态lambda线程的初始化安全吗? [英] C++ Multithreading: is initialization of a local static lambda thread safe?

查看:91
本文介绍了C ++多线程:本地静态lambda线程的初始化安全吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

C ++ 11标准对局部静态变量初始化进行了说明,认为它应该是线程安全的( http://en.cppreference.com/w/cpp/language/storage_duration#Static_local_variables ).我的问题涉及将lambda初始化为静态局部变量时究竟会发生什么?

The C++11 standard says about local static variable initialization that it is supposed to be thread safe (http://en.cppreference.com/w/cpp/language/storage_duration#Static_local_variables). My question concerns what exactly happens when a lambda is initialized as a static local variable?

让我们考虑以下代码:

#include <iostream>
#include <functional>

int doSomeWork(int input)
{
    static auto computeSum = [](int number)                                                                                                                                                                  
    {
      return 5 + number;
    };  
    return computeSum(input);
}

int main(int argc, char *argv[])
{
    int result = 0;
#pragma omp parallel
{
  int localResult = 0;
#pragma omp for
  for(size_t i=0;i<5000;i++)
  {
   localResult += doSomeWork(i);
  }
#pragma omp critical
{
   result += localResult;
}
}

std::cout << "Result is: " << result << std::endl;

return 0;
}

使用ThreadSanitizer与GCC 5.4编译:

compiled with GCC 5.4, using ThreadSanitizer:

gcc -std=c++11 -fsanitize=thread -fopenmp -g main.cpp -o main -lstdc++

工作正常,ThreadSanitizer没有给出任何错误.现在,如果我将lambda"computeSum"初始化的行更改为此:

Works fine, ThreadSanitizer gives no errors. Now, if I change the line where the lambda "computeSum" is initialized to this:

static std::function<int(int)> computeSum = [](int number)
{
  return 5 + number;
};  

代码仍然可以编译,但是ThreadSanitizer给我一个警告,指出存在数据争用:

The code still compiles, but ThreadSanitizer gives me a warning, saying there is a data race:

WARNING: ThreadSanitizer: data race (pid=20887)
  Read of size 8 at 0x000000602830 by thread T3:
    #0 std::_Function_base::_M_empty() const /usr/local/gcc-5.4_nofutex/include/c++/5.4.0/functional:1834 (main+0x0000004019ec)
    #1 std::function<int (int)>::operator()(int) const /usr/local/gcc-5.4_nofutex/include/c++/5.4.0/functional:2265 (main+0x000000401aa3)
    #2 doSomeWork(int) /home/laszlo/test/main.cpp:13 (main+0x000000401242)
    #3 main._omp_fn.0 /home/laszlo/test/main.cpp:25 (main+0x000000401886)
    #4 gomp_thread_start ../../../gcc-5.4.0/libgomp/team.c:118 (libgomp.so.1+0x00000000e615)

  Previous write of size 8 at 0x000000602830 by thread T1:
    #0 std::_Function_base::_Function_base() /usr/local/gcc-5.4_nofutex/include/c++/5.4.0/functional:1825 (main+0x000000401947)
    #1 function<doSomeWork(int)::<lambda(int)>, void, void> /usr/local/gcc-5.4_nofutex/include/c++/5.4.0/functional:2248 (main+0x000000401374)
    #2 doSomeWork(int) /home/laszlo/test/main.cpp:12 (main+0x000000401211)
    #3 main._omp_fn.0 /home/laszlo/test/main.cpp:25 (main+0x000000401886)
    #4 gomp_thread_start ../../../gcc-5.4.0/libgomp/team.c:118 (libgomp.so.1+0x00000000e615)

  Location is global 'doSomeWork(int)::computeSum' of size 32 at 0x000000602820 (main+0x000000602830)

  Thread T3 (tid=20891, running) created by main thread at:
    #0 pthread_create ../../../../gcc-5.4.0/libsanitizer/tsan/tsan_interceptors.cc:895 (libtsan.so.0+0x000000026704)
    #1 gomp_team_start ../../../gcc-5.4.0/libgomp/team.c:796 (libgomp.so.1+0x00000000eb5e)
    #2 __libc_start_main <null> (libc.so.6+0x00000002082f)

  Thread T1 (tid=20889, running) created by main thread at:
    #0 pthread_create ../../../../gcc-5.4.0/libsanitizer/tsan/tsan_interceptors.cc:895 (libtsan.so.0+0x000000026704)
    #1 gomp_team_start ../../../gcc-5.4.0/libgomp/team.c:796 (libgomp.so.1+0x00000000eb5e)
    #2 __libc_start_main <null> (libc.so.6+0x00000002082f)

SUMMARY: ThreadSanitizer: data race /usr/local/gcc-5.4_nofutex/include/c++/5.4.0/functional:1834 std::_Function_base::_M_empty() const

无论如何,ThreadSanitizer报告数据争用的代码需要执行5-10次,直到出现警告消息为止.

In any case, the code where ThreadSanitizer reports a data race needs to be executed 5-10 times until the warning mesage appears.

所以我的问题是:两者之间在概念上有区别吗

So my question is: is there a conceptional difference between

static auto computeSum = [](int number){ reentrant code returing int };

static std::function<int(int)> computeSum = [](int number) {same code returning int};

什么使第一个代码起作用而第二个代码成为数据竞争?

What makes the first code to work and the second to be a data race?

编辑#1 : 似乎已经有很多关于我的问题的讨论.我发现塞巴斯蒂安·雷德尔(Sebastian Redl)的贡献最有帮助,因此我接受了这个答案. 我只想总结一下,以便人们可以参考. (请让我知道这是否不适用于Stack Overflow,在这里我真的不问问题...)

Edit #1: It seems that there was (is) quite a discussion going on about my question. I found Sebastian Redl's contribution the most helpful, thus I accepted that answer. I just want to summarize, so that people can refer to this. (Please le me know if this is not appropriate on Stack Overflow, I do not really ask stuff here...)

为什么要报告数据竞赛?

(在MikeMB的评论中)建议该问题与TSAN的gcc实施中的错误有关(请参阅链接).似乎是正确的:

It was suggested in a comment (by MikeMB) that the problem is related to a bug in the gcc implementation of TSAN (see this and this link). It seems to be correct:

如果我编译包含以下内容的代码:

If I compile the code that contains:

static std::function<int(int)> computeSum = [](int number){ ... return int;};

在GCC 5.4中,机器代码如下:

with GCC 5.4, the machine code looks like:

  static std::function<int(int)> computeSum = [](int number)
  {
    return 5 + number;
  };
  4011d5:       bb 08 28 60 00          mov    $0x602808,%ebx
  4011da:       48 89 df                mov    %rbx,%rdi
  4011dd:       e8 de fd ff ff          callq  400fc0 <__tsan_read1@plt>
  ....

而在GCC 6.3中,其显示为:

whereas, with GCC 6.3, it reads:

  static std::function<int(int)> computeSum = [](int number)                                                                                                                                             
  {
    return 5 + number;
  };
  4011e3:   be 02 00 00 00          mov    $0x2,%esi
  4011e8:   bf 60 28 60 00          mov    $0x602860,%edi
  4011ed:   e8 9e fd ff ff          callq  400f90 <__tsan_atomic8_load@plt>

我不是机器代码的高手,但是看起来像在GCC 5.4版本中一样,__tsan_read1@plt用于检查静态变量是否已初始化.相比之下,GCC 6.3生成__tsan_atomic8_load@plt.我猜第二个是正确的,第一个会导致误报.

I am not a big master of machine code, but it looks like that in the GCC 5.4 version, __tsan_read1@plt is used to check whether the static variable is initialized. In comparison, GCC 6.3 generates __tsan_atomic8_load@plt . I guess the second one is correct, the first one leads to a false positive.

如果我在不使用ThreadSanitizer的情况下编译该版本,则GCC 5.4会生成:

If I compile the version without ThreadSanitizer, GCC 5.4 generates:

static std::function<int(int)> computeSum = [](int number)
{                                                                                                                                                                                                        
  return 5 + number;
};
400e17:     b8 88 24 60 00          mov    $0x602488,%eax
400e1c:     0f b6 00                movzbl (%rax),%eax
400e1f:     84 c0                   test   %al,%al
400e21:     75 4a                   jne    400e6d <doSomeWork(int)+0x64>
400e23:     bf 88 24 60 00          mov    $0x602488,%edi
400e28:     e8 83 fe ff ff          callq  400cb0 <__cxa_guard_acquire@plt>

和GCC 6.3:

  static std::function<int(int)> computeSum = [](int number)
  {                                                                                                                                                                                                      
    return 5 + number;
  };
  400e17:   0f b6 05 a2 16 20 00    movzbl 0x2016a2(%rip),%eax        # 6024c0 <guard variable for doSomeWork(int)::computeSum>
  400e1e:   84 c0                   test   %al,%al
  400e20:   0f 94 c0                sete   %al
  400e23:   84 c0                   test   %al,%al
  400e25:   74 4a                   je     400e71 <doSomeWork(int)+0x68>
  400e27:   bf c0 24 60 00          mov    $0x6024c0,%edi
  400e2c:   e8 7f fe ff ff          callq  400cb0 <__cxa_guard_acquire@plt>

如果我使用auto而不是std::function ,为什么没有数据争用?

Why is no data race, if I use auto instead of std::function?

您可能需要在此处进行更正,但是编译器可能会内联"自动对象,因此无需对静态对象是否已初始化进行记账.

You might have to correct me here, but probably the compiler "inlines" the auto object, so there is no need to do bookkeeping on whether the static object has been initialized or not.

static auto computeSum = [](int number){ ... return int;};

产生:

  static auto computeSum = [](int number)
  400e76:   55                      push   %rbp                                                                                                                                                          
  400e77:   48 89 e5                mov    %rsp,%rbp
  400e7a:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
  400e7e:   89 75 f4                mov    %esi,-0xc(%rbp)
  //static std::function<int(int)> computeSum = [](int number)
  {
    return 5 + number;
  };
  400e81:   8b 45 f4                mov    -0xc(%rbp),%eax
  400e84:   83 c0 05                add    $0x5,%eax
  400e87:   5d                      pop    %rbp
  400e88:   c3                      retq

推荐答案

C ++标准保证,无论多么复杂的局部静态初始化都是线程安全的,因为初始化代码将只运行一次,并且没有线程将在初始化完成之前运行经过初始化代码.

The C++ standard guarantees that initialization of local statics, no matter how complex, is thread-safe, in that the initialization code will run exactly once, and no thread will run past the initialization code before initialization is complete.

此外,从线程安全的角度来看,它保证调用std :: function是读取操作,这意味着只要std :: function对象为,任意数量的线程都可以同时执行此操作.不能同时修改.

Furthermore, it guarantees that invoking a std::function is a read operation from the view of thread safety, meaning that an arbitrary number of threads may do it at the same time, as long as the std::function object is not modified at the same time.

通过这些保证,并且由于您的代码不包含任何其他访问共享状态的内容,因此它应该是线程安全的.如果仍然触发TSan,则存在一个错误 somewhere :

By these guarantees, and because you code does not contain anything else that accesses shared state, it should be thread-safe. If it still triggers TSan, there's a bug somewhere:

  • 最有可能的是,GCC正在使用非常棘手的原子代码来进行静态初始化保证,而TSan无法将其识别为安全的.换句话说,这是TSan中的错误.确保您正在使用两种工具的最新版本. (具体地说,似乎TSan某种程度上缺少确保std::function的初始化实际上对其他线程可见的某种障碍.)
  • 不太可能,GCC的初始化魔术实际上是不正确的,并且这里存在真实的比赛条件.我找不到任何有关5.4具有此类错误的错误报告,该错误随后已得到修复.但是无论如何,您可能都想尝试使用较新版本的GCC. (最新版本是6.3.)
  • 有人建议std::function的构造函数可能存在一个错误,即它以不安全的方式访问全局共享方式.但是,即使这是真的,也没关系,因为您的代码不应多次调用构造函数.
  • 在GCC中,可能会将内含静态函数的函数内联到OpenMP并行循环中.也许这会导致静态变量的重复或安全初始化代码的破坏.为此,必须检查生成的组件.
  • Most likely, GCC is using very tricky atomic code for the static initialization guarantee, which TSan cannot identify as safe. In other words, it's a bug in TSan. Make sure you are using the most up-to-date versions of both tools that you can. (Specifically, it appears that TSan is somehow missing some kind of barrier that makes sure that the initialization of the std::function is actually visible to other threads.)
  • Less likely, GCC's initialization magic is actually incorrect, and there is a real race condition here. I cannot find any bug report to the effect that 5.4 had such a bug that was later fixed. But you might want to try with a newer version of GCC anyway. (Latest is 6.3.)
  • Some people have suggested that the constructor of std::function may have a bug where it accesses global shared way in an unsafe way. But even if this is true, it should not matter, because your code should not invoke the constructor more than once.
  • There may be a bug in GCC in inlining a function containing statics into an OpenMP parallel loop. Perhaps that leads to duplication of the statics, or breaking of the safe initialization code. Inspection of the generated assembly would be necessary for this.

顺便说一句,该代码的第一个版本是不同的,因为它是完全无关紧要的.在-O3下,GCC实际上将在编译时完全计算循环,从而将您的主要函数有效地转换为

The first version of the code is different, by the way, because it is completely trivial. Under -O3, GCC will actually completely compute the loop at compile-time, effectively converting your main function to

std::cout << "Result is: " << 12522500 << std::endl;

https://godbolt.org/g/JDRPQV

即使不这样做,也不会对lambda进行初始化(变量只是填充的单个字节),因此,没有对任何内容的写访问,也没有进行数据竞争的机会.

And even if it didn't do that, there is no initialization done for the lambda (the variable is just a single byte of padding), thus no write accesses to anything, and no opportunity for data races.

这篇关于C ++多线程:本地静态lambda线程的初始化安全吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆