c++11 race-condition memory-barriers thread-sanitizer

c++11 - ¿Por qué ThreadSanitizer informa una carrera con este ejemplo sin bloqueo?



race-condition memory-barriers (2)

El ThreadSanitizer no es bueno para contar, no puede entender que las escrituras en los elementos siempre ocurren antes de las lecturas.

El ThreadSanitizer puede encontrar que los almacenes de m_enqueueIndex ocurren antes de las cargas, pero no entiende que el almacén de items[m_dequeueIndex] debe ocurrir antes de la carga cuando tail > m_dequeueIndex .

He resumido esto en un simple ejemplo autocontenido. El hilo principal pone en cola 1000 elementos y un subproceso de trabajo intenta salir de la cola simultáneamente. ThreadSanitizer se queja de que hay una carrera entre la lectura y la escritura de uno de los elementos, a pesar de que existe una secuencia de barrera de memoria de liberación de adquisición que los protege.

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

Salida de 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

Línea de comando:

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

Versión g ++: 5.3.1

¿Alguien puede arrojar algo de luz sobre por qué Tsan cree que esto es una carrera de datos?

ACTUALIZAR

Parece que esto es un falso positivo. Para apaciguar a ThreadSanitizer, he agregado anotaciones (vea here para ver las que son compatibles y here para ver un ejemplo). Tenga en cuenta que la detección de si tsan está habilitado en GCC a través de una macro solo se ha agregado recientemente , por lo que tuve que pasar manualmente -D__SANITIZE_THREAD__ a g ++ por ahora.

#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

Ahora ThreadSanitizer está feliz en el tiempo de ejecución.


Esto se parece a https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78158 . Desmontar el binario producido por GCC muestra que no instrumenta las operaciones atómicas en O0. Como solución alternativa, puede compilar su código con GCC con -O1 / -O2, u obtener una compilación nueva de Clang y usarla para ejecutar ThreadSanitizer (esta es la forma recomendada, ya que TSan se está desarrollando como parte de Clang y solo backported a GCC).

Los comentarios anteriores no son válidos: TSan puede comprender fácilmente la relación de suceso anterior entre los elementos atómicos en su código (se puede verificar ejecutando el reproductor anterior bajo TSan en Clang).

Tampoco recomendaría usar AnnotateHappensBefore () / AnnotateHappensAfter () por dos razones:

  • No deberías necesitarlos en la mayoría de los casos; indican que el código está haciendo algo realmente complejo (en cuyo caso es posible que desee volver a verificar que lo está haciendo bien);

  • Si comete un error en su código de bloqueo, rociarlo con anotaciones puede enmascarar ese error, para que TSan no lo note.