1

The code snippet below is a minimal example of a pattern we use in our production code for doing asynchronous work. When a thread has been spawned, we want to make sure at some point that the work it was meant to do has been completed. We always join threads.

#include <future>
#include <iostream>
#include <thread> 

int main(void) {
    std::promise<void> p;
    std::shared_future<void> f = p.get_future().share();
    
    std::thread t = std::thread([&p] {
        p.set_value_at_thread_exit();
        std::cout << "Hello thread." << std::endl;
    });
    
    f.wait();
    std::cout << "Hello main." << std::endl;
    
    t.join();
    
    return 0; 
}

The problem I am seeing is that I get warnings from thread sanitizers (both clang and gcc) related to the use of set_value_at_thread_exit(). If I instead call p.set_value() at the end of the thread function body, there are no warnings. The output from Clang's ThreadSanitizer running this example is pasted at the end of this post.

Specifically, I want to understand

  1. Why set_value_at_thread_exit() yields a data race while set_value() does not? (I also tried the very similar example posted in https://en.cppreference.com/w/cpp/thread/promise/set_value_at_thread_exit; it behaves the same way)
  2. What will the functional difference be comparing the two approaches (given that we always join our threads)? Can we be sure that the work of the thread is complete if we call p.set_value() last thing before returning from the thread? (My guess is that we can)
WARNING: ThreadSanitizer: data race (pid=174249)
  Read of size 8 at 0x7b0c00000048 by main thread:
    #0 std::__uniq_ptr_impl<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter>::_M_ptr() const <null> (test+0xd4981) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #1 std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter>::get() const <null> (test+0xd4935) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #2 std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter>::operator*() const <null> (test+0xd8197) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #3 std::__future_base::_State_baseV2::wait() <null> (test+0xd812b) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #4 std::__basic_future<void>::wait() const <null> (test+0xd177f) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #5 main <null> (test+0xd0dc7) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)

  Previous write of size 8 at 0x7b0c00000048 by thread T1:
    #0 std::enable_if<__and_<std::__not_<std::__is_tuple_like<std::__future_base::_Result_base*>>, std::is_move_constructible<std::__future_base::_Result_base*>, std::is_move_assignable<std::__future_base::_Result_base*>>::value, void>::type std::swap<std::__future_base::_Result_base*>(std::__future_base::_Result_base*&, std::__future_base::_Result_base*&) <null> (test+0xd5145) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #1 std::__uniq_ptr_impl<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter>::swap(std::__uniq_ptr_impl<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter>&) <null> (test+0xd50ae) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #2 std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter>::swap(std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter>&) <null> (test+0xd48f5) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #3 std::__future_base::_State_baseV2::_M_do_set(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*) <null> (test+0xd5e53) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #4 void std::__invoke_impl<void, void (std::__future_base::_State_baseV2::*)(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*), std::__future_base::_State_baseV2*, std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*>(std::__invoke_memfun_deref, void (std::__future_base::_State_baseV2::*&&)(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*), std::__future_base::_State_baseV2*&&, std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*&&, bool*&&) <null> (test+0xd6801) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #5 std::__invoke_result<void (std::__future_base::_State_baseV2::*)(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*), std::__future_base::_State_baseV2*, std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*>::type std::__invoke<void (std::__future_base::_State_baseV2::*)(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*), std::__future_base::_State_baseV2*, std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*>(void (std::__future_base::_State_baseV2::*&&)(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*), std::__future_base::_State_baseV2*&&, std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*&&, bool*&&) <null> (test+0xd66c5) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #6 void std::call_once<void (std::__future_base::_State_baseV2::*)(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*), std::__future_base::_State_baseV2*, std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*>(std::once_flag&, void (std::__future_base::_State_baseV2::*&&)(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*), std::__future_base::_State_baseV2*&&, std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*&&, bool*&&)::'lambda'()::operator()() const <null> (test+0xd6640) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #7 std::once_flag::_Prepare_execution::_Prepare_execution<void std::call_once<void (std::__future_base::_State_baseV2::*)(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*), std::__future_base::_State_baseV2*, std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*>(std::once_flag&, void (std::__future_base::_State_baseV2::*&&)(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*), std::__future_base::_State_baseV2*&&, std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*&&, bool*&&)::'lambda'()>(void (std::__future_base::_State_baseV2::*&)(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*))::'lambda'()::operator()() const <null> (test+0xd6595) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #8 std::once_flag::_Prepare_execution::_Prepare_execution<void std::call_once<void (std::__future_base::_State_baseV2::*)(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*), std::__future_base::_State_baseV2*, std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*>(std::once_flag&, void (std::__future_base::_State_baseV2::*&&)(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*), std::__future_base::_State_baseV2*&&, std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*&&, bool*&&)::'lambda'()>(void (std::__future_base::_State_baseV2::*&)(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*))::'lambda'()::__invoke() <null> (test+0xd6525) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #9 pthread_once <null> (test+0x52ffa) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #10 __gthread_once(int*, void (*)()) main.cpp (test+0xd1473) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #11 void std::call_once<void (std::__future_base::_State_baseV2::*)(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*), std::__future_base::_State_baseV2*, std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*>(std::once_flag&, void (std::__future_base::_State_baseV2::*&&)(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*), std::__future_base::_State_baseV2*&&, std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*&&, bool*&&) <null> (test+0xd5d68) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #12 std::__future_base::_State_baseV2::_M_set_delayed_result(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>, std::weak_ptr<std::__future_base::_State_baseV2>) <null> (test+0xd5961) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #13 std::promise<void>::set_value_at_thread_exit() <null> (test+0xd5766) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #14 main::$_0::operator()() const main.cpp (test+0xd13d5) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #15 void std::__invoke_impl<void, main::$_0>(std::__invoke_other, main::$_0&&) main.cpp (test+0xd1375) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #16 std::__invoke_result<main::$_0>::type std::__invoke<main::$_0>(main::$_0&&) main.cpp (test+0xd12e5) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #17 void std::thread::_Invoker<std::tuple<main::$_0>>::_M_invoke<0ul>(std::_Index_tuple<0ul>) main.cpp (test+0xd129d) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #18 std::thread::_Invoker<std::tuple<main::$_0>>::operator()() main.cpp (test+0xd1245) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #19 std::thread::_State_impl<std::thread::_Invoker<std::tuple<main::$_0>>>::_M_run() main.cpp (test+0xd1159) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #20 execute_native_thread_routine /build/gcc-12-U8K4Qv/gcc-12-12.2.0/build/x86_64-linux-gnu/libstdc++-v3/src/c++11/../../../../../src/libstdc++-v3/src/c++11/thread.cc:82:18 (libstdc++.so.6+0xdc3a2) (BuildId: 83485f864cbc36aa1055a1a323e1706a347af35e)

  Location is heap block of size 48 at 0x7b0c00000030 allocated by main thread:
    #0 operator new(unsigned long) <null> (test+0xcfd86) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #1 std::__new_allocator<std::_Sp_counted_ptr_inplace<std::__future_base::_State_baseV2, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>>::allocate(unsigned long, void const*) <null> (test+0xd219e) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #2 std::allocator_traits<std::allocator<std::_Sp_counted_ptr_inplace<std::__future_base::_State_baseV2, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>>>::allocate(std::allocator<std::_Sp_counted_ptr_inplace<std::__future_base::_State_baseV2, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>>&, unsigned long) <null> (test+0xd2059) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #3 std::__allocated_ptr<std::allocator<std::_Sp_counted_ptr_inplace<std::__future_base::_State_baseV2, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>>> std::__allocate_guarded<std::allocator<std::_Sp_counted_ptr_inplace<std::__future_base::_State_baseV2, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>>>(std::allocator<std::_Sp_counted_ptr_inplace<std::__future_base::_State_baseV2, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>>&) <null> (test+0xd1d8e) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #4 std::__shared_count<(__gnu_cxx::_Lock_policy)2>::__shared_count<std::__future_base::_State_baseV2, std::allocator<void>>(std::__future_base::_State_baseV2*&, std::_Sp_alloc_shared_tag<std::allocator<void>>) <null> (test+0xd1bed) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #5 std::__shared_ptr<std::__future_base::_State_baseV2, (__gnu_cxx::_Lock_policy)2>::__shared_ptr<std::allocator<void>>(std::_Sp_alloc_shared_tag<std::allocator<void>>) <null> (test+0xd1b4b) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #6 std::shared_ptr<std::__future_base::_State_baseV2>::shared_ptr<std::allocator<void>>(std::_Sp_alloc_shared_tag<std::allocator<void>>) <null> (test+0xd1aaf) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #7 std::shared_ptr<std::enable_if<!is_array<std::__future_base::_State_baseV2>::value, std::__future_base::_State_baseV2>::type> std::make_shared<std::__future_base::_State_baseV2>() <null> (test+0xd1921) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #8 std::promise<void>::promise() <null> (test+0xd1590) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #9 main <null> (test+0xd0d73) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)

  Thread T1 (tid=174251, finished) created by main thread at:
    #0 pthread_create <null> (test+0x4fd6d) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)
    #1 __gthread_create /build/gcc-12-U8K4Qv/gcc-12-12.2.0/build/x86_64-linux-gnu/libstdc++-v3/include/x86_64-linux-gnu/bits/gthr-default.h:663:35 (libstdc++.so.6+0xdc478) (BuildId: 83485f864cbc36aa1055a1a323e1706a347af35e)
    #2 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State>>, void (*)()) /build/gcc-12-U8K4Qv/gcc-12-12.2.0/build/x86_64-linux-gnu/libstdc++-v3/src/c++11/../../../../../src/libstdc++-v3/src/c++11/thread.cc:147:37 (libstdc++.so.6+0xdc478)
    #3 main <null> (test+0xd0db9) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d)

SUMMARY: ThreadSanitizer: data race (/home/magnus/slask/test_set_value_at_thread_exit_sanitize/test+0xd4981) (BuildId: 835e6cd1a0a63985f82e88f16af4d885c75ac85d) in std::__uniq_ptr_impl<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter>::_M_ptr() const

Magnus
  • 141
  • 2
  • 6
  • 1
    Just to make sure: You did build the standard library with thread sanitizer instrumentation as well? – user17732522 Aug 04 '23 at 12:08
  • 1
    "_Can we be sure that the work of the thread is complete if we call p.set_value() last thing before returning from the thread?_": No, the arguments given to `std::invoke` to call the thread function may have introduced temporaries that are destructed afterwards. The return value of the thread function may introduce a temporary that will be destroyed afterwards. Also thread-local variables will be destroyed afterwards. – user17732522 Aug 04 '23 at 12:15
  • @user17732522 No, I have not compiled the standard library myself. I was of the understanding you did not have to recompile supporting libraries for thread sanitizer to work. – Magnus Aug 04 '23 at 12:21
  • 1
    Thread sanitizer requires all libraries, including the standard ones, to be compiled with its instrumentation. Otherwise there will be false positives. See https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual. – user17732522 Aug 04 '23 at 13:04

0 Answers0