2016-05-31 20 views
12

L'ho ridotto a un semplice esempio indipendente. Il thread principale accoda 1000 elementi e un thread worker tenta di disconnettersi contemporaneamente. ThreadSanitizer si lamenta che c'è una corsa tra la lettura e la scrittura di uno degli elementi, anche se c'è una sequenza di protezione della memoria acquisita che li protegge.Perché ThreadSanitizer segnala una gara con questo esempio di blocco?

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

uscita 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 

Riga di comando:

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

g ++ versione: 5.3.1

Qualcuno può far luce sul motivo per cui tsan pensa che questo sia un dato gara?


UPDATE

Sembra che questo è un falso positivo. Per placare ThreadSanitizer, ho aggiunto annotazioni (vedere here per quelle supportate e here per un esempio). Si noti che per rilevare se tsan è abilitato in GCC tramite una macro è only recently been added, quindi ho dovuto passare manualmente a -D__SANITIZE_THREAD__ a g ++ per ora.

#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 

Ora ThreadSanitizer è felice in fase di esecuzione.

+1

Fa la differenza, se si utilizza la coerenza sequenziale per gli accessi atomici? – MikeMB

+0

No, Tsan riporta ancora una gara. – Cameron

+0

Penso che il tuo "AGGIORNAMENTO" sia in realtà una risposta - e buona! Prendi in considerazione la possibilità di spostarlo dalla domanda e in una risposta. –

risposta

4

Il ThreadSanitizer non è valido per il conteggio, non può capire che le scritture sugli articoli avvengano sempre prima delle letture.

Il ThreadSanitizer può trovare che i negozi di m_enqueueIndex accadere prima del caricamento, ma non capisce che il negozio per items[m_dequeueIndex] deve avvenire prima che il carico quando tail > m_dequeueIndex.

+1

Si tratta di una limitazione di progettazione di 'ThreadSanitizer', o questo comportamento dovrebbe essere segnalato come bug/difetto? –

+0

@VittorioRomeo È un difetto, ma è di progettazione. Manterrà il comportamento corrente fino a quando qualcuno non troverà un nuovo algoritmo in grado di gestire questo caso in modo efficiente. – user1887915

+0

Ah, non mi ero reso conto che ThreadSanitizer poteva produrre falsi positivi. Questo non è affatto chiaro nella documentazione che ho trovato :-) La carta che hai collegato descrive l'originale ThreadSanitizer; a quanto ho capito, è stato riscritto come uno strumento compilatore/runtime integrato invece di uno basato su valgrind. Non sono sicuro quali parti siano ancora applicabili. Vedrò se posso annotare il mio codice per rendere felice lo tsan. – Cameron

2

Questo assomiglia a https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78158. Disassemblare il binario prodotto da GCC mostra che non strumenta le operazioni atomiche su O0. Come soluzione, puoi creare il tuo codice con GCC con -O1/-O2, oppure ottenere una nuova build Clang e usarla per eseguire ThreadSanitizer (questo è il metodo consigliato, dato che TSan viene sviluppato come parte di Clang e solo backported a GCC).

I commenti sopra non sono validi: TSan può facilmente comprendere la relazione prima-accade tra gli atomici nel codice (è possibile verificarlo eseguendo il riproduttore sopra in TSan in Clang).

anche io non mi consiglia di utilizzare l'AnnotateHappensBefore()/AnnotateHappensAfter() per due motivi:

  • non si dovrebbe bisogno nella maggior parte dei casi; indicano che il codice sta facendo qualcosa di veramente complesso (nel qual caso potresti voler ricontrollare che lo stai facendo bene);

  • se si commette un errore nel codice di blocco, irrorandolo con annotazioni può mascherare tale errore, in modo che TSan non lo noti.

Problemi correlati