Анализ потокобезопасности
Движок аннотирует обычные mutex с помощью Clang Thread Safety Analysis (TSA), чтобы неправильная работа с блокировками, например обращение к защищённому состоянию без mutex, отсутствие требуемой capability или возврат с удерживаемой блокировкой, становилась ошибкой компиляции на каждом toolchain Clang. Анализ статический и не имеет runtime-стоимости; он дополняет, но не заменяет runtime-проверки и тесты конкурентности.
TSA является дополнительной защитой для обычных mutex с лексической областью действия. Он намеренно не моделирует кооперативные и динамически получаемые схемы блокировок; см. раздел «Исключения».
Об exception safety при получении блокировки, в частности почему постусловие выданного
EntityLockиспользуетFO_STRONG_ASSERT, см. Exception Safety.
Toolchain и обязательность
- В
BuildTools/cmake/stages/Init.cmakeанализ включён для каждого compiler id Clang: nativeclang,clang-cl, AppleClang, Emscripten и Android NDK. Используются-Wthread-safety -Werror=thread-safety, а для cl-style драйвераclang-clфлаги передаются через/clang:. MSVC и GCC TSA не реализуют, поэтому авторитетным gate является сборка Clang. - Макросы
FO_TSA_*разворачиваются в__attribute__((...))только при__clang__; для остальных компиляторов это no-op, поэтому аннотированный код без изменений собирается MSVC/GCC. - Сторонние библиотеки подавляются через
DisableLibWarnings(-wи-Wno-error=для legacy-C ошибок по умолчанию в Clang 20+), поэтому TSA анализирует только собственный код движка и подключающего проекта.
Зачем нужны обёртки mutex из fo::
Проект использует платформенную STL, а libc++ отключена. Ни MS STL, ни
libstdc++ не аннотируют std::mutex / std::shared_mutex как capabilities.
Поэтому FO_TSA_GUARDED_BY(std_mutex_member) выдал бы сообщение о требуемой
capability и ничего не проверил. Защищённое состояние должно использовать
аннотированные примитивы движка из Source/Essentials/Threading.h.
Это прямые замены std-аналогов с теми же именами в snake_case и теми же
методами, поэтому в месте блокировки достаточно заменить std:: на fo:::
| Тип | Оборачивает / повторяет | Назначение |
|---|---|---|
fo::mutex |
std::mutex |
состояние только с exclusive-доступом |
fo::shared_mutex |
std::shared_mutex |
состояние с reader/writer-доступом |
fo::atomic_mutex |
park/wake поверх атомарного состояния | короткие critical sections из noexcept code, где ошибку получения OS mutex невозможно сообщить |
fo::scoped_lock<T> |
std::scoped_lock / std::lock_guard |
exclusive RAII guard для mutex или shared_mutex; CTAD: scoped_lock lk {m} |
fo::shared_lock<T> |
std::shared_lock |
shared reader guard для shared_mutex |
fo::unique_lock<T> |
std::unique_lock |
exclusive guard с ручными lock()/unlock(), пригодный для std::condition_variable_any |
std::scoped_lock, std::unique_lock и std::shared_lock непрозрачны для
анализатора в платформенной STL, поэтому в проверяемых местах нужны guard из
fo::. Код движка находится внутри FO_BEGIN_NAMESPACE, где имена применяются
без квалификатора. Подключайте Threading.h; он расположен низко в Essentials,
сразу над HashedString, и доступен даже низкоуровневым заголовкам.
Condition variables используют std::condition_variable_any, принимающий
любой Lockable, включая fo::unique_lock. Передавайте guard прямо в wait:
unique_lock lock {_dataLocker};
_workSignal.wait(lock, [this]() FO_TSA_REQUIRES(_dataLocker) { return _ready; });
Аннотирование mutex
- Используйте
mutex/shared_mutexи объявляйте его до защищаемых полей. - Добавьте
FO_TSA_GUARDED_BY(_locker)ко всем защищённым полям данных. - В местах блокировки замените
std::scoped_lock/std::lock_guardнаscoped_lock,std::shared_lockнаshared_lock, обычный exclusivestd::unique_lockнаscoped_lock, аstd::unique_lockс condition variable или ручным relock наunique_lock; сам cv замените наstd::condition_variable_any. Всегда используйте brace initialization:scoped_lock lock {mutex};,shared_lock lock {mutex};. - На private helper, предполагающий уже удерживаемую блокировку, ставьте
FO_TSA_REQUIRES(_locker)для exclusive илиFO_TSA_REQUIRES_SHARED(_locker)для read-доступа. - У hand-written RAII guard пометьте класс
FO_TSA_SCOPED_CAPABILITY, конструкторFO_TSA_ACQUIRE(mutex), деструкторFO_TSA_RELEASE(); move constructor, если он есть, требуетFO_TSA_NO_ANALYSIS.
Не помещайте атрибут между
()и trailing-> type. Для аннотированных методов используйте ведущий return type, напримерbool try_lock() FO_TSA_TRY_ACQUIRE(true).
fo::thread, то есть handle задачи пула и результатthreading::run_thread, также объявлен вThreading.hв namespacefo.
Словарь макросов
FO_TSA_CAPABILITY(name), FO_TSA_SCOPED_CAPABILITY, FO_TSA_GUARDED_BY(x),
FO_TSA_PT_GUARDED_BY(x), FO_TSA_ACQUIRED_BEFORE(...),
FO_TSA_ACQUIRED_AFTER(...), FO_TSA_REQUIRES(...),
FO_TSA_REQUIRES_SHARED(...), FO_TSA_ACQUIRE(...),
FO_TSA_ACQUIRE_SHARED(...), FO_TSA_RELEASE(...),
FO_TSA_RELEASE_SHARED(...), FO_TSA_RELEASE_GENERIC(...),
FO_TSA_TRY_ACQUIRE(...), FO_TSA_TRY_ACQUIRE_SHARED(...),
FO_TSA_EXCLUDES(...), FO_TSA_ASSERT_CAPABILITY(x),
FO_TSA_ASSERT_SHARED_CAPABILITY(x), FO_TSA_RETURN_CAPABILITY(x),
FO_TSA_NO_ANALYSIS.
Исключения: что TSA не покрывает
std::recursive_mutex: повторное получение нельзя смоделировать. Recursive locks остаются сырымиstd::recursive_mutexбезGUARDED_BYи с комментарием// recursive: not modelable by TSA.- Однопоточные init/teardown-проходы, которые обходят защищённое состояние,
повторно входя в locking-код и поэтому не могут удерживать lock: помечайте
функцию и каждую внутреннюю lambda отдельно как
FO_TSA_NO_ANALYSIS, добавляя комментарий, почему выполнение однопоточное. Используйте редко и никогда для сокрытия настоящей гонки. - Кооперативные или динамически получаемые наборы блокировок, где набор зависит от данных и получается нелексически, выразить нельзя. Не аннотируйте их и документируйте исключение в документе владеющей подсистемы.
Подключающие проекты описывают собственный перечень защищённых полей и проектные исключения в своей документации по потокам. Эта страница определяет только переиспользуемый механизм.