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
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:
std::function
is actually visible to other threads.)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.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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With