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 является операцией чтения-изменения-записи, это должно применяться здесь.
Чтобы уточнить, порядок операций, которые мы должны обеспечить, выглядит следующим образом:
- Уменьшение refcount до значения> 1
- Уменьшение рефконта до 1
- Удаление объекта
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]).