C++11 (санация потока g++) Упорядочивание неатомарных операций с атомами (ложное срабатывание?)

Я экспериментирую с g ++ и обработчиком потоков и думаю, что получаю ложные срабатывания. Это правда или я совершаю большую ошибку?

Программа (вырезано и вставлено Энтони Уильямсом: параллелизм C++ в действии, стр. 145, листинг 5.13)

#include <atomic>
#include <thread>
#include <assert.h>
bool x=false;
std::atomic<bool> y;
std::atomic<int> z;
void write_x_then_y()
{
  x=true;
  std::atomic_thread_fence(std::memory_order_release);
  y.store(true,std::memory_order_relaxed);
}
void read_y_then_x()
{
  while(!y.load(std::memory_order_relaxed));
  std::atomic_thread_fence(std::memory_order_acquire);
  if(x)
    ++z;
}
int main()
{
  x=false;
  y=false;
  z=0;
  std::thread a(write_x_then_y);
  std::thread b(read_y_then_x);
  a.join();
  b.join();
  assert(z.load()!=0);
}

Составлено с:

g++ -o a -g -Og -pthread a.cpp -fsanitize=thread

версия g ++

~/build/px> g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/6.1.1/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --enable-bootstrap --enable-languages=c,c++,objc,obj-c++,fortran,ada,go,lto --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-shared --enable-threads=posix --enable-checking=release --enable-multilib --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-plugin --enable-initfini-array --disable-libgcj --with-isl --enable-libmpx --enable-gnu-indirect-function --with-tune=generic --with-arch_32=i686 --build=x86_64-redhat-linux
Thread model: posix
gcc version 6.1.1 20160621 (Red Hat 6.1.1-3) (GCC)

Я получаю:

~/build/px> ./a
==================
WARNING: ThreadSanitizer: data race (pid=13794)
  Read of size 1 at 0x000000602151 by thread T2:
    #0 read_y_then_x() /home/ostri/build/px/a.cpp:17 (a+0x000000401014)
    #1 void std::_Bind_simple<void (*())()>::_M_invoke<>(std::_Index_tuple<>) /usr/include/c++/6.1.1/functional:1400 (a+0x000000401179)
    #2 std::_Bind_simple<void (*())()>::operator()() /usr/include/c++/6.1.1/functional:1389 (a+0x000000401179)
    #3 std::thread::_State_impl<std::_Bind_simple<void (*())()> >::_M_run() /usr/include/c++/6.1.1/thread:196 (a+0x000000401179)
    #4 <null> <null> (libstdc++.so.6+0x0000000baaae)

  Previous write of size 1 at 0x000000602151 by thread T1:
    #0 write_x_then_y() /home/ostri/build/px/a.cpp:9 (a+0x000000400fbd)
    #1 void std::_Bind_simple<void (*())()>::_M_invoke<>(std::_Index_tuple<>) /usr/include/c++/6.1.1/functional:1400 (a+0x000000401179)
    #2 std::_Bind_simple<void (*())()>::operator()() /usr/include/c++/6.1.1/functional:1389 (a+0x000000401179)
    #3 std::thread::_State_impl<std::_Bind_simple<void (*())()> >::_M_run() /usr/include/c++/6.1.1/thread:196 (a+0x000000401179)
    #4 <null> <null> (libstdc++.so.6+0x0000000baaae)

  Location is global 'x' of size 1 at 0x000000602151 (a+0x000000602151)

  Thread T2 (tid=13797, running) created by main thread at:
    #0 pthread_create <null> (libtsan.so.0+0x000000028380)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0x0000000badc4)
    #2 main /home/ostri/build/px/a.cpp:26 (a+0x000000401097)

  Thread T1 (tid=13796, finished) created by main thread at:
    #0 pthread_create <null> (libtsan.so.0+0x000000028380)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0x0000000badc4)
    #2 main /home/ostri/build/px/a.cpp:25 (a+0x00000040108a)

SUMMARY: ThreadSanitizer: data race /home/ostri/build/px/a.cpp:17 in read_y_then_x()
==================
ThreadSanitizer: reported 1 warnings

Я получил это предупреждение в более сложной программе, и я подумал, что это моя ошибка, но теперь даже "школьная программа" отображает то же самое поведение. Это (то есть какой-то переключатель компилятора отсутствует) меня или 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

#include <atomic>
#include <thread>
#include <assert.h>
bool x=false;
std::atomic<bool> y;
std::atomic<int> z;
void write_x_then_y()
{
  x=true;
  std::atomic_thread_fence(std::memory_order_release);
  TSAN_ANNOTATE_HAPPENS_BEFORE(&x);
  y.store(true,std::memory_order_relaxed);
}
void read_y_then_x()
{
  while(!y.load(std::memory_order_relaxed));
  std::atomic_thread_fence(std::memory_order_acquire);
  TSAN_ANNOTATE_HAPPENS_AFTER(&x);
  if(x)
    ++z;
}
{
  x=false;
  y=false;
  z=0;
  std::thread a(write_x_then_y);
  std::thread b(read_y_then_x);
  a.join();
  b.join();
  assert(z.load()!=0);
}

Команда компиляции

g++ -o a -g -Og -pthread a.cpp -fsanitize=thread -D__SANITIZE_THREAD__

3 ответа

Решение

TL;DR: это ложное срабатывание TSAN. Код действителен.

Тема 1:

  x=true;                                              // W
  std::atomic_thread_fence(std::memory_order_release); // A
  y.store(true,std::memory_order_relaxed);             // X

Тема 2:

  while(!y.load(std::memory_order_relaxed));           // Y
  std::atomic_thread_fence(std::memory_order_acquire); // B
  if(x)                                                // R
     ++z;

[atomics.fences] / 2:

Ограничение разблокировки A синхронизируется с ограничителем получения B, если существуют атомарные операции X и Y, обе из которых работают с некоторым атомарным объектом M, так что A секвенируется до X, X модифицирует M, Y секвенируется до B, а Y считывает значение записанное X или значение, записанное любым побочным эффектом в гипотетической последовательности деблокирования X, сработало бы, если бы это была операция деблокирования.

Давайте пройдемся по списку:

  • [✔] "существуют атомарные операции X и Y, которые работают на некотором атомарном объекте M": очевидно. М является y,
  • [✔] "A упорядочен перед X": очевидно ( [intro.execution] / 14 для тех, кто хочет цитирования).
  • [✔] "Х изменяет М": очевидно.
  • [✔] "Y секвенируется перед B": очевидно.
  • [✔] "и Y читает значение, записанное X...": это единственный способ, которым цикл может завершиться.

Следовательно, разделительное ограждение A синхронизируется с разделительным ограждением B.

Запись W секвенируется перед A, а чтение R - после B, поэтому W -поток происходит до, и так происходит до R. [intro.races] / 9-10:

Оценка A -потока происходит перед оценкой B, если

  • А синхронизируется с В или
  • A упорядочен по зависимости перед B, или
  • для некоторой оценки X
    • A синхронизируется с X, а X упорядочивается перед B, или
    • A секвенируется до того, как X и X между потоками произойдет до B, или
    • Интерпоток происходит до того, как X и X межпоток происходит до B.

Оценка A происходит до оценки B (или, что то же самое, B происходит после A), если:

  • A секвенируется перед B, или
  • Интер-поток происходит до B.

Гонка данных не происходит из-за отношения "происходит до" ( [intro.races] / 19):

Выполнение программы содержит гонку данных, если она содержит два потенциально одновременных конфликтующих действия, по крайней мере одно из которых не является атомарным, и ни одно не происходит раньше другого, за исключением специального случая для обработчиков сигналов, описанных ниже. Любая такая гонка данных приводит к неопределенному поведению.

Кроме того, чтение R гарантированно считывает значение, записанное W, потому что W является видимым побочным эффектом, другого побочного эффекта нет x после начала потоков ( [intro.races] / 11):

Видимый побочный эффект A для скалярного объекта или битового поля M по отношению к вычислению значения B для M удовлетворяет условиям:

  • А происходит до В и
  • нет другого побочного эффекта от X до M, так что A происходит до X, а X происходит до B.

Значение неатомарного скалярного объекта или битового поля M, как определено оценкой B, должно быть значением, сохраненным видимым побочным эффектом A.

memory_order_relaxed не накладывает никаких ограничений на изменение порядка.

memory_order_acquire не мешает переупорядочить забор сверху. Это только предотвращает заказ снизу. Это означает, что код может быть выполнен так:

std::atomic_thread_fence(std::memory_order_acquire);
if(x)
  ++z;
while(!y.load(std::memory_order_relaxed));

Это приведет к гонке данных, поскольку чтение в if(x) расы с x=true,

Вам нужны заборы с memory_order_acq_rel или же memory_order_seq_cst семантика в обеих функциях, которая предотвращает изменение порядка в обоих направлениях.

К сожалению ThreadSanitizer не может постичь ограждения памяти. Это объясняется тем, что в терминах отношения "происходит до" между доступами к определенным объектам это обосновано, и в операции забора нет объекта.

Если вы замените смягченный груз + приобретающий забор на приобретающий груз, а освобождающий забор + расслабленный магазин - на склад освобождения, TSan правильно обнаружит отношение "случай до" между хранилищем и грузом.

Также обратите внимание, что реализация TSAN в GCC может не привести к атомарности инструмента в O0 (см. /questions/30295090/pochemu-threadsanitizer-soobschaet-o-gonke-s-etim-primerom-bez-blokirovki/30295099#30295099).

Другие вопросы по тегам