ThreadSanitizer сообщает "гонка данных при удалении оператора (void*)" при использовании встроенного счетчика ссылок

Пожалуйста, посмотрите на следующий код:

#include <pthread.h>
#include <boost/atomic.hpp>

class ReferenceCounted {
  public:
    ReferenceCounted() : ref_count_(1) {}

    void reserve() {
      ref_count_.fetch_add(1, boost::memory_order_relaxed);
    }

    void release() {
      if (ref_count_.fetch_sub(1, boost::memory_order_release) == 1) {
        boost::atomic_thread_fence(boost::memory_order_acquire);
        delete this;
      }
    }

  private:
    boost::atomic<int> ref_count_;
};

void* Thread1(void* x) {
  static_cast<ReferenceCounted*>(x)->release();
  return NULL;
}

void* Thread2(void* x) {
  static_cast<ReferenceCounted*>(x)->release();
  return NULL;
}

int main() {
  ReferenceCounted* obj = new ReferenceCounted();
  obj->reserve(); // for Thread1
  obj->reserve(); // for Thread2
  obj->release(); // for the main()
  pthread_t t[2];
  pthread_create(&t[0], NULL, Thread1, obj);
  pthread_create(&t[1], NULL, Thread2, obj);
  pthread_join(t[0], NULL);
  pthread_join(t[1], NULL);
}

Это несколько похоже на пример подсчета ссылок из Boost.Atomic.

Основные отличия заключаются в том, что встроенный ref_count_ инициализируется в 1 в конструкторе (когда конструктор завершен, мы имеем единственную ссылку на ReferenceCounted объект) и что код не использует boost::intrusive_ptr,

Пожалуйста, не вините меня за использование delete this в коде - это шаблон, который у меня есть в большой кодовой базе на работе, и сейчас я ничего не могу с этим поделать.

Теперь этот код скомпилирован с clang 3.5 из транка (подробности ниже) и ThreadSanitizer ( tsan v2) приводит к следующему выводу из ThreadSanitizer:

WARNING: ThreadSanitizer: data race (pid=9871)
  Write of size 1 at 0x7d040000f7f0 by thread T2:
    #0 operator delete(void*) <null>:0 (a.out+0x00000004738b)
    #1 ReferenceCounted::release() /home/A.Romanek/tmp/tsan/main.cpp:15 (a.out+0x0000000a2c06)
    #2 Thread2(void*) /home/A.Romanek/tmp/tsan/main.cpp:29 (a.out+0x0000000a2833)

  Previous atomic write of size 4 at 0x7d040000f7f0 by thread T1:
    #0 __tsan_atomic32_fetch_sub <null>:0 (a.out+0x0000000896b6)
    #1 boost::atomics::detail::base_atomic<int, int, 4u, true>::fetch_sub(int, boost::memory_order) volatile /home/A.Romanek/tmp/boost/boost_1_55_0/boost/atomic/detail/gcc-atomic.hpp:499 (a.out+0x0000000a3329)
    #2 ReferenceCounted::release() /home/A.Romanek/tmp/tsan/main.cpp:13 (a.out+0x0000000a2a71)
    #3 Thread1(void*) /home/A.Romanek/tmp/tsan/main.cpp:24 (a.out+0x0000000a27d3)

  Location is heap block of size 4 at 0x7d040000f7f0 allocated by main thread:
    #0 operator new(unsigned long) <null>:0 (a.out+0x000000046e1d)
    #1 main /home/A.Romanek/tmp/tsan/main.cpp:34 (a.out+0x0000000a286f)

  Thread T2 (tid=9874, running) created by main thread at:
    #0 pthread_create <null>:0 (a.out+0x00000004a2d1)
    #1 main /home/A.Romanek/tmp/tsan/main.cpp:40 (a.out+0x0000000a294e)

  Thread T1 (tid=9873, finished) created by main thread at:
    #0 pthread_create <null>:0 (a.out+0x00000004a2d1)
    #1 main /home/A.Romanek/tmp/tsan/main.cpp:39 (a.out+0x0000000a2912)

SUMMARY: ThreadSanitizer: data race ??:0 operator delete(void*)
==================
ThreadSanitizer: reported 1 warnings

Странно то, что thread T1 выполняет запись размером 1 в ту же ячейку памяти, что и thread T2 при выполнении атомарного декремента на контрольном счетчике.

Как объяснить предыдущую запись? Это какая-то очистка, выполненная деструктором ReferenceCounted учебный класс?

Это ложный позитив? Или код неправильный?

Моя настройка:

$ uname -a
Linux aromanek-laptop 3.13.0-29-generic #53-Ubuntu SMP Wed Jun 4 21:00:20 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

$ clang --version
Ubuntu clang version 3.5-1ubuntu1 (trunk) (based on LLVM 3.5)
Target: x86_64-pc-linux-gnu
Thread model: posix

Код скомпилирован так:

clang++ main.cpp -I/home/A.Romanek/tmp/boost/boost_1_55_0 -pthread -fsanitize=thread -O0 -g -ggdb3 -fPIE -pie -fPIC

Обратите внимание, что на моей машине реализация boost::atomic<T> решает в __atomic_load_n семейство функций, которые, как утверждает ThreadSanitizer, понимают.

ОБНОВЛЕНИЕ 1: то же самое происходит при использовании clang 3.4 Окончательный релиз.

ОБНОВЛЕНИЕ 2: та же проблема возникает с -std=c++11 а также <atomic> как с libstdC++, так и с libC++.

2 ответа

Решение

Это выглядит как ложный позитив.

thread_fence в release() метод обеспечивает выполнение всех ожидающих записей от fetch_sub-призывы к случаю - до того, как забор вернется. Следовательно delete на следующей строке нельзя состязаться с предыдущими записями из-за уменьшения повторного счета.

Цитата из книги C++ "Параллельность в действии":

Операция освобождения синхронизируется с забором с order из std::memory_order_acquire [...] если эта операция освобождения сохраняет значение, которое считывается атомарной операцией перед забором, в том же потоке, что и забор.

Поскольку уменьшение refcount является операцией чтения-изменения-записи, это должно применяться здесь.

Чтобы уточнить, порядок операций, которые мы должны обеспечить, выглядит следующим образом:

  1. Уменьшение refcount до значения> 1
  2. Уменьшение рефконта до 1
  3. Удаление объекта

2. а также 3. синхронизируются неявно, так как они происходят в одном потоке. 1. а также 2. синхронизируются, так как они являются атомарными операциями чтения-изменения-записи с одним и тем же значением. Если бы эти двое могли участвовать в гонке, весь пересчет был бы в первую очередь нарушен. Так что осталось синхронизировать 1. а также 3.,

Это именно то, что делает забор. Запись от 1. это release операция, которая, как мы только что обсуждали, синхронизирована с 2.читать на то же значение. 3., acquire забор на той же нити, что и 2., теперь синхронизируется с записью из 1. как гарантировано спецификацией. Это происходит без необходимости добавления acquire запись в объект (как это было предложено @KerrekSB в комментариях), которая также будет работать, но потенциально может быть менее эффективной из-за дополнительной записи.

Итог: не играйте с порядками памяти. Даже эксперты ошибаются, и их влияние на производительность зачастую незначительно. Так что, если вы не доказали в профилировании, что они убивают вашу производительность, и вам абсолютно положительно нужно оптимизировать это, просто притворитесь, что они не существуют, и придерживайтесь значения по умолчанию memory_order_seq_cst,

Просто чтобы подчеркнуть комментарий @adam-romanek для тех, кто наткнулся на это, во время написания (март 2018 г.) ThreadSanitizer не поддерживает автономные ограждения памяти. Это упоминается в FAQ по ThreadSanitizer, в котором явно не упоминается, что ограждения поддерживаются:

Q: Какие примитивы синхронизации поддерживаются? TSan поддерживает примитивы синхронизации pthread, встроенные атомарные операции компилятора (sync/atomic), операции C++ поддерживаются с помощью llvm libC++ (хотя и не очень тщательно протестировано [sic]).

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