2016-05-31 13 views
12

Ich habe dies auf ein einfaches in sich abgeschlossenes Beispiel heruntergekocht. Der Hauptthread reiht 1000 Elemente in die Warteschlange ein, und ein Worker-Thread versucht gleichzeitig, die Warteschlange aufzuheben. ThreadSanitizer beklagt sich, dass es einen Wettlauf zwischen dem Lesen und dem Schreiben eines der Elemente gibt, obwohl es eine Speicherbarrierensequenz zum Akquirieren und Freigeben gibt, die sie schützt.Warum meldet ThreadSanitizer ein Rennen mit diesem lock-free Beispiel?

#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(); 
} 

ThreadSanitizer Ausgang:

================== 
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 

Befehlszeile:

g++ -std=c++11 -O0 -g -fsanitize=thread issue49.cpp -o issue49 -pthread 

g ++ Version: 5.3.1

Kann jemand etwas Licht auf vergießen, warum tsan denkt, dies ist ein Daten Rennen?


UPDATE

Es scheint, wie diese um einen Fehlalarm handelt. Um ThreadSanitizer zu beschwichtigen, habe ich Anmerkungen hinzugefügt (siehe here für die unterstützten und here für ein Beispiel). Beachten Sie, dass die Erkennung, ob tsan in GCC über ein Makro aktiviert ist, only recently been added hat, also musste ich -D__SANITIZE_THREAD__ manuell an g ++ übergeben.

#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 

Jetzt ist ThreadSanitizer glücklich zur Laufzeit.

+1

Macht es einen Unterschied, wenn Sie sequentielle Konsistenz für die atomaren Zugriffe verwenden? – MikeMB

+0

Nein, Tsan meldet immer noch ein Rennen. – Cameron

+0

Ich denke, Ihr "UPDATE" ist eigentlich eine Antwort - und eine gute! Ziehen Sie in Betracht, es aus der Frage und in eine Antwort zu verschieben. –

Antwort

4

Die ThreadSanitizer ist nicht gut zu zählen, kann es nicht verstehen, dass schreibt auf die Elemente immer vor dem Lesevorgang passieren.

Der ThreadSanitizer kann feststellen, dass die Speicher von m_enqueueIndex passieren, bevor die Lasten, aber es versteht nicht, dass der Speicher items[m_dequeueIndex] vor dem Laden erfolgen muss, wenn .

+1

Ist das eine Designbeschränkung von 'ThreadSanitizer', oder sollte dieses Verhalten als Bug/Defekt gemeldet werden? –

+0

@VittorioRomeo Es ist ein Defekt, aber ist von Entwurf. Es wird das aktuelle Verhalten beibehalten, bis jemand einen _new_ Algorithmus findet, der diesen Fall effizient behandeln kann. – user1887915

+0

Ah, ich wusste nicht, dass ThreadSanitizer falsche Positive produzieren könnte. Das ist in der Dokumentation, die ich gefunden habe, gar nicht klar :-) Das von Ihnen verlinkte Papier beschreibt den ursprünglichen ThreadSanitizer; Wie ich es verstehe, wurde es als ein Compiler/Runtime-integriertes Tool anstelle von einem auf Valgrind basiert geschrieben. Ich bin mir nicht sicher, welche Teile noch anwendbar sind. Ich werde sehen, ob ich meinen Code kommentieren kann, um tsan glücklich zu machen. – Cameron

2

Das sieht wie https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78158 aus. Das Zerlegen der von GCC erzeugten Binärdatei zeigt, dass es die atomaren Operationen auf O0 nicht instrumentiert. Als Workaround können Sie entweder Ihren Code mit GCC mit -O1/-O2 erstellen, oder Sie erhalten einen neuen Clang-Build und verwenden ihn zum Ausführen von ThreadSanitizer (dies ist die empfohlene Methode, da TSan als Teil von Clang und nur nach GCC zurückportiert).

Die obigen Kommentare sind ungültig: TSan kann die Vorkommnis-Relation zwischen den Atomics in Ihrem Code leicht nachvollziehen (das kann man überprüfen, indem man den obigen Player unter TSan in Clang ausführt).

Ich würde auch nicht empfehlen, die AnnotateHappensBefore mit()/AnnotateHappensAfter() aus zwei Gründen:

  • Sie sollten sie nicht in den meisten Fällen müssen; Sie zeigen an, dass der Code etwas wirklich Komplexes tut (in diesem Fall sollten Sie überprüfen, ob Sie es richtig machen);

  • Wenn Sie einen Fehler in Ihrem Lock-Free-Code machen, kann der Fehler durch Besprühen mit Annotationen diesen Fehler maskieren, so dass TSan dies nicht bemerkt.