为什么ThreadSanitizer使用这个无锁示例举报竞赛?

Cam*_*ron 20 race-condition memory-barriers c++11 thread-sanitizer

我已将其归结为一个简单的自包含示例.主线程将1000个项目排队,并且工作线程尝试同时出列.ThreadSanitizer抱怨在其中一个元素的读取和写入之间存在竞争,即使存在一个保护它们的获取释放内存屏障序列.

#include <atomic>
#include <thread>
#include <cassert>

struct FakeQueue
{
    int items[1000];
    std::atomic<int> m_enqueueIndex;
    int m_dequeueIndex;

    FakeQueue() : m_enqueueIndex(0), m_dequeueIndex(0) { }

    void enqueue(int x)
    {
        auto tail = m_enqueueIndex.load(std::memory_order_relaxed);
        items[tail] = x;              // <- element written
        m_enqueueIndex.store(tail + 1, std::memory_order_release);
    }

    bool try_dequeue(int& x)
    {
        auto tail = m_enqueueIndex.load(std::memory_order_acquire);
        assert(tail >= m_dequeueIndex);
        if (tail == m_dequeueIndex)
            return false;
        x = items[m_dequeueIndex];    // <- element read -- tsan says race!
        ++m_dequeueIndex;
        return true;
    }
};


FakeQueue q;

int main()
{
    std::thread th([&]() {
        int x;
        for (int i = 0; i != 1000; ++i)
            q.try_dequeue(x);
    });

    for (int i = 0; i != 1000; ++i)
        q.enqueue(i);

    th.join();
}
Run Code Online (Sandbox Code Playgroud)

ThreadSanitizer输出:

==================
WARNING: ThreadSanitizer: data race (pid=17220)
  Read of size 4 at 0x0000006051c0 by thread T1:
    #0 FakeQueue::try_dequeue(int&) /home/cameron/projects/concurrentqueue/tests/tsan/issue49.cpp:26 (issue49+0x000000402bcd)
    #1 main::{lambda()#1}::operator()() const <null> (issue49+0x000000401132)
    #2 _M_invoke<> /usr/include/c++/5.3.1/functional:1531 (issue49+0x0000004025e3)
    #3 operator() /usr/include/c++/5.3.1/functional:1520 (issue49+0x0000004024ed)
    #4 _M_run /usr/include/c++/5.3.1/thread:115 (issue49+0x00000040244d)
    #5 <null> <null> (libstdc++.so.6+0x0000000b8f2f)

  Previous write of size 4 at 0x0000006051c0 by main thread:
    #0 FakeQueue::enqueue(int) /home/cameron/projects/concurrentqueue/tests/tsan/issue49.cpp:16 (issue49+0x000000402a90)
    #1 main /home/cameron/projects/concurrentqueue/tests/tsan/issue49.cpp:44 (issue49+0x000000401187)

  Location is global 'q' of size 4008 at 0x0000006051c0 (issue49+0x0000006051c0)

  Thread T1 (tid=17222, running) created by main thread at:
    #0 pthread_create <null> (libtsan.so.0+0x000000027a67)
    #1 std::thread::_M_start_thread(std::shared_ptr<std::thread::_Impl_base>, void (*)()) <null> (libstdc++.so.6+0x0000000b9072)
    #2 main /home/cameron/projects/concurrentqueue/tests/tsan/issue49.cpp:41 (issue49+0x000000401168)

SUMMARY: ThreadSanitizer: data race /home/cameron/projects/concurrentqueue/tests/tsan/issue49.cpp:26 FakeQueue::try_dequeue(int&)
==================
ThreadSanitizer: reported 1 warnings
Run Code Online (Sandbox Code Playgroud)

命令行:

g++ -std=c++11 -O0 -g -fsanitize=thread issue49.cpp -o issue49 -pthread
Run Code Online (Sandbox Code Playgroud)

g ++版本:5.3.1

任何人都可以解释为什么tsan认为这是一场数据竞赛?


UPDATE

这似乎是误报.为了安抚ThreadSanitizer,我添加了注释(请参阅此处获取支持的注释,此处为示例).请注意,检测是否通过宏在GCC中启用了tsan 最近才被添加,因此我现在必须手动传递-D__SANITIZE_THREAD__给g ++.

#if defined(__SANITIZE_THREAD__)
#define TSAN_ENABLED
#elif defined(__has_feature)
#if __has_feature(thread_sanitizer)
#define TSAN_ENABLED
#endif
#endif

#ifdef TSAN_ENABLED
#define TSAN_ANNOTATE_HAPPENS_BEFORE(addr) \
    AnnotateHappensBefore(__FILE__, __LINE__, (void*)(addr))
#define TSAN_ANNOTATE_HAPPENS_AFTER(addr) \
    AnnotateHappensAfter(__FILE__, __LINE__, (void*)(addr))
extern "C" void AnnotateHappensBefore(const char* f, int l, void* addr);
extern "C" void AnnotateHappensAfter(const char* f, int l, void* addr);
#else
#define TSAN_ANNOTATE_HAPPENS_BEFORE(addr)
#define TSAN_ANNOTATE_HAPPENS_AFTER(addr)
#endif

struct FakeQueue
{
    int items[1000];
    std::atomic<int> m_enqueueIndex;
    int m_dequeueIndex;

    FakeQueue() : m_enqueueIndex(0), m_dequeueIndex(0) { }

    void enqueue(int x)
    {
        auto tail = m_enqueueIndex.load(std::memory_order_relaxed);
        items[tail] = x;
        TSAN_ANNOTATE_HAPPENS_BEFORE(&items[tail]);
        m_enqueueIndex.store(tail + 1, std::memory_order_release);
    }

    bool try_dequeue(int& x)
    {
        auto tail = m_enqueueIndex.load(std::memory_order_acquire);
        assert(tail >= m_dequeueIndex);
        if (tail == m_dequeueIndex)
            return false;
        TSAN_ANNOTATE_HAPPENS_AFTER(&items[m_dequeueIndex]);
        x = items[m_dequeueIndex];
        ++m_dequeueIndex;
        return true;
    }
};

// main() is as before
Run Code Online (Sandbox Code Playgroud)

现在ThreadSanitizer在运行时很开心.

Gli*_*der 7

看起来像https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78158。分解由GCC生成的二进制文件表明,它不能对O0进行原子操作。解决方法是,您可以使用带有-O1 / -O2的GCC来构建代码,也可以使用全新的Clang构建并使用它来运行ThreadSanitizer(这是推荐的方法,因为TSan正在作为Clang的一部分开发,并且仅回传到GCC)。

上面的注释是无效的:TSan可以轻松理解代码中原子之间的事前关系(可以通过在Clang中的TSan下运行上述复制器来进行检查)。

由于两个原因,我也不建议使用AnnotateHappensBefore()/ AnnotateHappensAfter():

  • 在大多数情况下,您不需要它们;它们表示代码正在做的事情确实很复杂(在这种情况下,您可能需要仔细检查您做的是否正确);

  • 如果您在无锁代码中出错,则在其上加上注解可能会掩盖该错误,以便TSan不会注意到它。


use*_*915 5

ThreadSanitizer不擅长计算,也不能明白,写入项目总是发生在读取之前.

ThreadSanitizer可以发现存储m_enqueueIndex在加载之前发生,但它不明白存储items[m_dequeueIndex]必须在加载之前发生tail > m_dequeueIndex.

  • ThreadSanitizer确实可以产生误报,但这不是这种情况(参见下面的评论).不幸的是,关于基于Valgrind的ThreadSanitizer的论文不再适用,而且没有"新"的论文涵盖事物的实际状态.您可以参考YouTube上的各种讲座(搜索"ThreadSanitizer")对于该工具的工作现在一个简要说明. (3认同)
  • 这是"ThreadSanitizer"的设计限制,还是应该将此行为报告为错误/缺陷? (2认同)
  • 啊,我没有意识到 ThreadSanitizer 会产生误报。这在我找到的文档中一点也不清楚:-) 您链接的论文描述了原始的 ThreadSanitizer;据我了解,它已被重写为编译器/运行时集成工具,而不是基于 valgrind 的工具。我不确定哪些部分仍然适用。我会看看我是否可以注释我的代码以使 tsan 高兴。 (2认同)