C++11 为什么ThreadSanitarizer会用这个无锁示例报告一个竞争?

C++11 为什么ThreadSanitarizer会用这个无锁示例报告一个竞争?,c++11,race-condition,memory-barriers,thread-sanitizer,C++11,Race Condition,Memory Barriers,Thread Sanitizer,我将此归结为一个简单的独立示例。主线程将1000个项目排入队列,工作线程尝试同时退出队列。ThreadSanitarizer抱怨其中一个元素的读写之间存在竞争,即使有一个获取-释放内存屏障序列保护它们 #include <atomic> #include <thread> #include <cassert> struct FakeQueue { int items[1000]; std::atomic<int> m_enqueu

我将此归结为一个简单的独立示例。主线程将1000个项目排入队列,工作线程尝试同时退出队列。ThreadSanitarizer抱怨其中一个元素的读写之间存在竞争,即使有一个获取-释放内存屏障序列保护它们

#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();
}
g++版本:5.3.1

有人能解释一下为什么曾荫权认为这是一场数据竞赛吗


更新

这似乎是一个假阳性。为了安抚ThreadSanitizer,我添加了注释(请参阅以获取支持的注释和示例)。请注意,通过宏检测是否在GCC中启用了tsan,因此我现在必须手动将
-D_uusanitize\u THREAD_u
传递给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
#如果已定义(uu清理_u线程)
#定义TSAN_已启用
#elif已定义(_具有_特征)
#如果\uuuu具有\u功能(线程\u消毒剂)
#定义TSAN_已启用
#恩迪夫
#恩迪夫
#ifdef TSAN_已启用
#定义之前发生的事件注释(addr)\
注释发生在(uuuu文件,uuu行,uuuuu,(void*)(addr))之前
#定义TSAN_注释发生在(addr)之后\
注释发生在(uuuu文件,uuu行,uuuuu,(void*)(addr))之后
外部“C”void annotatesBefore(const char*f,int l,void*addr);
外部“C”void注释发生在(const char*f,int l,void*addr);
#否则
#定义之前发生的事件注释(addr)
#定义TSAN_注释发生在(addr)之后
#恩迪夫
结构伪造队列
{
国际项目[1000];
std::原子m_排队索引;
int m_dequeueIndex;
FakeQueue():m_排队索引(0),m_排队索引(0){}
无效排队(整数x)
{
auto tail=m_enqueueIndex.load(std::memory_order_released);
项目[尾]=x;
TSAN_注释_发生在前(&items[tail]);
m_enqueueIndex.store(tail+1,std::memory\u order\u release);
}
bool try_dequeue(int&x)
{
auto tail=m_enqueueIndex.load(std::memory\u order\u acquire);
断言(tail>=m_dequeueIndex);
if(tail==m_dequeueIndex)
返回false;
TSAN_注释_发生在(&items[m_dequeueIndex])之后;
x=项目[m_dequeueIndex];
++m_-dequeueIndex;
返回true;
}
};
//main()和以前一样
现在ThreadSanitizer在运行时很开心。

不擅长计数,它无法理解对项目的写入总是在读取之前发生

ThreadSanitarizer可以发现
m_-enqueueIndex
的存储发生在加载之前,但它不理解
items[m_-dequeueIndex]
的存储必须发生在加载之前,如图所示。分解GCC生成的二进制文件表明,它不会对O0执行原子操作。 作为一种解决方法,您可以使用GCC和-O1/-O2构建代码,或者获得一个新的clangbuild并使用它运行ThreadSanitizer(这是推荐的方法,因为TSan是Clang的一部分,并且只向后移植到GCC)

上面的评论是无效的:TSan可以很容易地理解代码中原子之间的before关系(可以通过在TSan的叮当声中运行上面的复制器来检查这一点)

我也不建议使用AnnotateHappensBefore()/AnnotateHappensAfter(),原因有二:

  • 在大多数情况下,你不应该需要它们;它们表示代码正在做一些非常复杂的事情(在这种情况下,您可能需要再次检查您是否做对了)

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


如果您在原子访问中使用顺序一致性,这会有区别吗?不,tsan仍然报告了一场竞赛。我认为您的“更新”实际上是一个答案,而且是一个很好的答案!请考虑把它从这个问题中移开,然后变成一个答案。托比:谢谢,但实际上它只是一个详细的例子,说明如何在我的具体示例代码中沉默假阳性。user1887915给出了真正的答案(tsan目前首先不支持这种类型的代码)。番茄/番茄酱;-)这是
ThreadSanitarizer
的设计限制,还是应将此行为报告为错误/缺陷?@VittorioRomeo这是一个缺陷,但是设计造成的。它将保持当前的行为,直到有人找到一种新的算法来有效地处理这种情况。啊,我没有意识到ThreadSanitarizer会产生误报。这在我找到的文档中一点也不清楚:-)你链接的那篇文章描述了最初的ThreadSanitarizer;据我所知,它被重写为一个编译器/运行时集成工具,而不是一个基于valgrind的工具。我不确定哪些部件仍然适用。我将看看我是否可以注释我的代码以使tsan高兴。ThreadSanitizer确实可以产生误报,但这里的情况并非如此(请参阅下面的注释)。不幸的是,关于基于Valgrind的ThreadSanitarizer的论文不再适用,也没有“新”的论文涵盖实际情况。您可以参考YouTube上的各种对话(搜索“ThreadSanitarizer”),了解该工具目前的工作原理。
g++ -std=c++11 -O0 -g -fsanitize=thread issue49.cpp -o issue49 -pthread
#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