![]() |
Arene Base
Fundamental Utilities For Safety Critical C++
|
Thread Safety Analysis (TSA hereafter) is a form of static analysis supported by the clang compiler which, through use of explicit user annotations, allows the compiler to detect many types of common concurrency defects statically at compile time.
-Wthread-safety compiler flag.The annotations are implemented in the form of macros which are used to decorate class definitions, function/member-function declarations, and variable/member declarations. This domain specific language is based on the notion of "capabilities" that guard resources, and then correctly annotating which functions interact with which capabilities.
clang supports TSA. However, the attributes are safe to use with other compilers in production settings; they turn into no-ops for unsupported compilers. Because the attributes provide a declarative syntax for documenting the concurrency model of a system, it is therefore recommended to use them even if clang is not the primary compiler for the project.The fundamental axiom of Thread Safety Analysis is that of the capability, which protects resources. Resources can be global variables, members, or functions/methods, and a calling thread may not access a resource unless it holds the required capability. This conceptual model, and TSA's analysis in general, is based on the concepts of Capability Based Security.
In practice, there are specifically annotated C++ types which may act as capabilities. A mutex is the most basic example of a type which may act as a capability; if a resource is said to be guarded by the mutex, then attempting to access the resource without the mutext being locked (without acquiring the capability) will result in a compile-time error.
This is only a brief summary of capabilities and the analysis model for TSA, please see the full documentation for more details.
There are two primary annotations for declaring resources as being guarded by capabilities:
| Annotation | Annotation Applied To | Description |
|---|---|---|
ARENE_TSA_GUARDED_BY(x) | Global Variables/Members | Annotates a variable/member as as being guarded by the named capability |
ARENE_TSA_PT_GUARDED_BY(x) | Pointer-Like Global Variables/Members | Annotates the content pointed to by a pointer-like variable/member as as being guarded by the named capability. The pointer-like entity itself is not guarded |
Their usage is straightforward, as demonstrated in the following example:
A common issue in concurrent programming is deadlock: a thread enters a function which acquires exclusive access to a capability in its body, and then before releasing the capability, enters another function which attempts to acquire the capability again, becoming forever blocked on itself.
TSA provides annotations for function/method bodies that allow deadlock detection to be pushed to compile time, by declaring that a capability is required to be held, or excluded from being held, in order to enter the function/method:
| Annotation | Annotation Applied To | Description |
|---|---|---|
ARENE_TSA_REQUIRES(...) | Functions/Methods | Annotates a function/method as requiring the calling thread to hold exclusive access to the named capabilities before entering and after exit |
ARENE_TSA_REQUIRES_SHARED(...) | Functions/Methods | Annotates a function/method as requiring the calling thread to hold shared access to the named capabilities before entering and after exit |
ARENE_TSA_EXCLUDES(...) | Functions/Methods | Annotates a function/method as excluding the calling thread from holding the named capabilities before entering and after exit |
Put into simple terms: a function/method annotated with ARENE_TSA_REQUIRES[_SHARED] is declaring it will not attempt to acquire the capabilities that guard the resources it might access during its execution. Whereas one annotated with ARENE_TSA_EXCLUDES is declaring it will acquire the capabilities that guard the resources it attempts to access during its execution. An example is shown below:
This two-step design of having a public, capability excluding method that simply acquires the capability and then dispatches to a private, capability requiring implementation becomes a common pattern to avoid deadlock while allowing functionality reuse; other methods which also hold update_guard_ can safely call update_message_impl() without fear of deadlock or data races. If update_message() is incorrectly called while already holding update_guard_, a compiler error is raised instead of deadlock resulting. Similarly, if update_message_impl() is called without holding update_guard_, a compiler error is raised instead of introducing a data race.
update_message_impl() was not annotated with ARENE_TSA_REQUIRES(update_guard_), a compiler error would still be issued if the calling thread did not hold update_guard_; this is because msg_ is annotated as TSA_GUARDED_BY(update_guard_). However this compiler error would point to the wrong place: it would point to the access of the guarded resource, msg_ = std::move(msg). This isn't where the actual defect is though, since update_message_impl() is supposed to be able to access the resource. With the ARENE_TSA_REQUIRES(update_guard_) annotation, the compiler error points to the correct place: the call to update_message_impl() without holding the needed update_guard_ capability.There are occasions where due to known limitations in TSA, a function/method may need to be excluded from TSA analysis. This can be achieved using the ARENE_TSA_NO_THREAD_SAFETY_ANALYSIS annotation, like so:
This excludes the function from TSA analysis entirely.
ARENE_TSA_NO_THREAD_SAFETY_ANALYSIS is part of the implementation rather than the interface of a function/method. It thus should be placed with the definition -in a .cpp file for non-inline code- rather than the declaration.There are a number of limitations in the current implementation of TSA, beyond the most obvious of currently only being part of the clang compiler and required explicitly annotated capabilities to work. A bulleted summary list follows, for full descriptions of each please see the official documentation:
TSA annotations on methods are parsed at the same time as method bodies, and thus are subject to the same lexical scoping limitations with respect to use-before-declaration when referencing a named capability.
TSA annotations respect visibility restrictions. A private mutex member cannot be named in an annotation in a context that would not normally have visibility to that member. The ARENE_TSA_RETURN_CAPABILITY(x) annotation can provide a workaround, see the linked documentation for details.
TSA's analysis is not path-sensitive to branches dependant on runtime state, so situations where a capability may be conditionally held at a given point in a program may cause spurious diagnostics.
ASSERT_XXX macros return on failure. This can result in unexpectedly hitting the conditional locking limitations in unit tests if the release of a capability is done only if an assert passes, rather than unconditionally via a scoped capability tied to the scope containing the assert.As ctors and dtors can generally assumed to be executing in the context of a single thread for a given instance, and typically have to initialize or destroy guarded resources, all ctors and dtors are implicitly treated as if annotated with ARENE_TSA_NO_THREAD_SAFETY_ANALYSIS.
TSA is strictly intra-procedural in the same way as type-checking: it relies purely on the declared attributes of variables/members/methods/functions, it does not examine their implementations. This can cause issues when type-erasure or similar indirection hides the nessisary calls to acquire or release a given capability: for example if a callback is consumed as arene::base::function_ref<void() const>, and that callback acquires or releases a capability referenced by the outer scope, TSA will not see this happen and spurious warnings may result.
template<typename Func> or similar, and thus do not type erase the signature of the callback, annotating the input lambda as appropriate will "do the right thing." Annotations on lambdas look like this: [mtx_, resource_]() ARENE_TSA_EXCLUDES(mtx_) { /* acquire mtx, use resource */ }Other than inside a SCOPED_CAPABILITY, TSA cannot see through pointer aliases to capabilities to understand they are equivalent:
TSA defines attributes for establishing an order that capabilities must be acquired, but they are currently unimplemented in the actual analysis. To avoid confusion and a false sense of security, arene-base has not exposed them.
There is currently no mechanism to support recursive capability acquisition. While this limitation is not stated explicitly in the documentation, it is likely due to the same reasons that conditional locking is not supported: recursion must necessarily be conditional to not recurse endlessly, and that condition is runtime state dependent.
That said, explicit recursion is against MISRA/AutoSAR guidelines, and nearly all other need for recursive mutexes can be avoided by following the public/private indirection pattern mentioned in the function annotation section.
unique_lockThere are several limitations that combine to make it impossible to correctly annotate std::unique_lock or an equivalent type to work with TSA:
std::unique_lock effectively is an alias for a mutex; that's its reason d'etre. You can move them, swap them, re-assign them. All of these are runtime state considerations that TSA simply cannot follow based on static program information.std::unique_lock allows recursive mutexes.This can make std::condition_variable difficult to use correctly under TSA, as std::unique_lock is generally the best practice for the lock type to pass to it. There are two possible workarounds:
std::unique_lock. In this case, due to the lack of inlining, the lambda passed to std::condition_variable must be annotated as ARENE_TSA_REQUIRES(mtx), where mtx is the capability which guards the resources accessed in the lambda.Use arene::base::scoped_lock_assertion to manually set TSA into the correct state without actually changing the lock state:
std::lock and scoped_lockThe "no aliases" limitation also prevents reasonably annotating std::lock and std::scoped_lock. Unfortunately users will have to fall back to individual arene::base::lock_guard usages. The need for the deadlock prevention mechanism of std::lock is lessened by the fact that TSA would have already detected the deadlock at compile time.
arene-base provides pre-annotated implementations of common concurrency primitives in its mutex_subpackage mutex subpackage. Prefer those where possible.There are 11 annotations that are used to declare a type as providing a capability:
| Annotation | Annotation Applied To | Description |
|---|---|---|
ARENE_TSA_CAPABILITY(x) | Classes | Annotates a class as providing a capability |
ARENE_TSA_SCOPED_CAPABILITY | Classes | Annotates a class as providing a capability tied to a scope |
ARENE_TSA_ACQUIRE(...) | Functions/Methods | Annotates a function/method as acquiring exclusive access to the named capability(s) before the function exits |
ARENE_TSA_ACQUIRE_SHARED(...) | Functions/Methods | Annotates a function/method as acquiring shared access to the named capability(s) before the function exits |
ARENE_TSA_TRY_ACQUIRE(...) | Functions/Methods | Annotates a function/method as one which will attempt to acquire exclusive access to the named capability(s) before the function exits. The return value shall be a boolean indicating success or failure to acquire the capability. |
ARENE_TSA_TRY_ACQUIRE_SHARED(...) | Functions/Methods | Annotates a function/method as one which will attempt to acquire shared access to the named capability(s) before the function exits. The return value shall be a boolean indicating success or failure to acquire the capability. |
ARENE_TSA_RELEASE(...) | Functions/Methods | Annotates a function/method as releasing exclusive access to the named capability(s) before the function exits |
ARENE_TSA_RELEASE_SHARED(...) | Functions/Methods | Annotates a function/method as releasing shared access to the named capability(s) before the function exits |
ARENE_TSA_RELEASE_GENERIC(...) | Functions/Methods | Annotates a function/method as releasing access to the named capability(s), regardless of exclusivity, before the function exits |
ARENE_TSA_ASSERT_CAPABILITY(x) | Functions/Methods | Annotates a function/method as one which will check if the calling thread has exclusive access to the named capability(s) before entering the function, and if it does not would cause program abandonment. |
ARENE_TSA_ASSERT_SHARED_CAPABILITY(x) | Functions/Methods | Annotates a function/method as one which will check if the calling thread has shared access to the named capability(s) before entering the function, and if it does not would cause program abandonment. |
The usage of these annotations is best demonstrated through a simple example, which decorates the interface of a basic mutex that supports exclusive and shared access, taken from the TSA documentation:
A scoped capability is one which provides RAII semantics, in a manner similar to std::lock_guard. scoped_capabilities generally acquire a capability in their constructor, and release it in their destructor. The most basic example looks like this: