Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Multithreading: is initialization of a local static lambda thread safe? [duplicate]

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?

Let's consider the following code:

#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;
}

compiled with GCC 5.4, using ThreadSanitizer:

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

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;
};  

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

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 };

and

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?

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...)

Why is a data race reported?

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;};

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>
  ....

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>

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.

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>

And 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>

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;};

produces:

  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
like image 506
user2583614 Avatar asked Feb 06 '17 07:02

user2583614


2 Answers

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.

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.

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:

  • 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.

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

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.

like image 189
Sebastian Redl Avatar answered Oct 21 '22 11:10

Sebastian Redl


The reasoning is wrong in both the answers posted so far.

It has nothing to do with lambda being function pointer. The reason is: if a function doesn't access unprotected shared data, then it is safe. In the case of auto computeSum= .. as defined in the question, which is simple, the ThreadSanitizer easily proves that it does not access any shared data. However, in case of std::function case, the code becomes a bit complex, and the sanitizer is either confused, or simply doesn't go to the extent to prove that it is still the same! It just gives up, seeing the std::function. Or it has bug — or worse, std::function is buggy!

Lets do this experiment: define int global = 100; at global namespace, and then do ++global; in the first lambda. See what the sanitizer says now. I believe it will give warning/error! That is enough to prove that it has nothing to do with lambda being function pointer as claimed by other answers.

As for your question:

Is initialization of a local static lambda thread safe?

Yes (since C++11). Please search this site for more detailed answers. This has been discussed many times.

like image 24
Nawaz Avatar answered Oct 21 '22 13:10

Nawaz