dimanche 17 juillet 2022

Ignore false positives on std::future in valgrind (DRD)

I am writing some library code using std::future used to "beam" execution context of a lambda (or std::function /TBP) to another thread in order to avoid data races (because that std::function's operation would almost entirely be related to data managed by the other thread). This is basically the reference example usage of std::promise/std::future along with explicit exception passing, see below for the code structure.

However, when testing with DRD (from Valgrind tools), I see strange data races reported. According to https://en.cppreference.com/w/cpp/thread/promise/set_value and https://en.cppreference.com/w/cpp/thread/promise/get_future all this is supposed to be fine and not require any additional synchronization. For testing purposes, I have even added a std::mutex and lock_guard around promise/future access and the results were even more confusing, then my own mutex was reported as cause of the conflicting operations. I tried with GCC 12 and Clang 13, same picture.

So why does valgrind mark std::promise/std::future operations as unsafe and how can I work around that? Is there a way to whitelist/justify/hide that?

(This is Debian OS btw, libstdc++6 12.1.0-5)


==45452== Thread 2:
==45452== Conflicting load by thread 2 at 0x05c64800 size 4
==45452==    at 0x4928B55: load (atomic_base.h:488)
==45452==    by 0x4928B55: _M_load (atomic_futex.h:86)
==45452==    by 0x4928B55: std::__atomic_futex_unsigned<2147483648u>::_M_load_and_test_until(unsigned int, unsigned int, bool, std::memory_order, bool, std::chrono::duration<long, std::ratio<1l, 1l> >, std::chrono::duration<long, std::ratio<1l, 1000000000l> >) (atomic_futex.h:113)
==45452==    by 0x49287FC: std::__atomic_futex_unsigned<2147483648u>::_M_load_and_test(unsigned int, unsigned int, bool, std::memory_order) (atomic_futex.h:158)
==45452==    by 0x4928697: _M_load_when_equal (atomic_futex.h:212)
==45452==    by 0x4928697: std::__future_base::_State_baseV2::wait() (future:337)
==45452==    by 0x4959849: std::__basic_future<unsigned long>::_M_get_result() const (future:720)
==45452==    by 0x4955E55: std::future<unsigned long>::get() (future:806)
==45452==    by 0x4954AD5: RunOnIoThread(std::function<unsigned long ()>, unsigned long) (schedlib.cc:335)
...
==45452== Address 0x5c64800 is at offset 32 from 0x5c647e0. Allocation context:
==45452==    at 0x484514F: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_drd-amd64-linux.so)
==45452==    by 0x4925A4C: 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*) (new_allocator.h:137)
==45452==    by 0x49259A0: allocate (allocator.h:183)
==45452==    by 0x49259A0: 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) (alloc_traits.h:464)
==45452==    by 0x4925840: 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> >&) (allocated_ptr.h:98)
==45452==    by 0x4925750: 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> >) (shared_ptr_base.h:969)
==45452==    by 0x49256F6: 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> >) (shared_ptr_base.h:1712)
==45452==    by 0x49256B4: std::shared_ptr<std::__future_base::_State_baseV2>::shared_ptr<std::allocator<void>>(std::_Sp_alloc_shared_tag<std::allocator<void> >) (shared_ptr.h:464)
==45452==    by 0x49255F6: std::shared_ptr<std::__future_base::_State_baseV2> std::make_shared<std::__future_base::_State_baseV2>() (shared_ptr.h:1009)
==45452==    by 0x4955D08: std::promise<unsigned long>::promise() (future:1080)
==45452==    by 0x49549EB: RunOnIoThread(std::function<unsigned long ()>, unsigned long) (schedlib.cc:307)
...
==45452== Other segment start (thread 1)
==45452==    at 0x4854114: pthread_mutex_unlock (in /usr/libexec/valgrind/vgpreload_drd-amd64-linux.so)
==45452==    by 0x4914602: __gthread_mutex_unlock(pthread_mutex_t*) (gthr-default.h:779)
==45452==    by 0x4916D44: std::mutex::unlock() (std_mutex.h:118)
==45452==    by 0x4914807: std::lock_guard<std::mutex>::~lock_guard() (std_mutex.h:235)
==45452==    by 0x49546FA: PeekNewJobs(int, short, void*) (schedlib.cc:237)
... // this is basically the job processing on IO thread, taking scheduled tasks and running them
MyValues RunOnIoThread(std::function<MyValues()> act, MyValues onRejection = MyValues::INVOCATION_ERROR)
{
    if (!act) MyValues::BAD_ARGUMENT;
    // if called from IO thread, stay there
    if(ThisIsIoThread()) return act();

    std::promise<MyValues> pro;
    try {
        ScheduleOnIoThread([&]() {
            try {
                auto res = act();
                pro.set_value(res);
            }
            catch (...) { pro.set_exception(std::current_exception()); }
        });
    }
    catch (...) { return onRejection; }
    return pro.get_future().get(); // rethrows exception if stored there
}

Aucun commentaire:

Enregistrer un commentaire