Zircon code takes advantage of clang's thread safety analysis feature to document and machine-verify some of our synchronization invariants. These annotations are checked when building for clang (see getting started for instructions on building with clang).
In Zircon, we provide our own set of macros wrapping the annotations and have
annotated our synchronization primitives. When writing new code involving
synchronization or annotating existing code, in most cases you should use the
thread annotation macros provided by
<lib/zircon-internal/thread_annotations.h.
These macros all begin with the prefix "TA_"
for thread analysis. The most
commonly used ones are:
TA_GUARDED(x)
the annotated variable is guarded by the capability (e.g. lock)x
TA_ACQ(x...)
function acquires all of the mutexes in the setx
and hold them after returningTA_REL(x...)
function releases all of the mutexes in the setx
TA_REQ(x...)
function requires that the caller hold all of the mutexes in the setx
TA_EXCL(x...)
function requires that the caller not be holding any of the mutexes in the setx
For example, a class containing a member variable 'int foo_'
protected by a
mutex would be annotated like so:
// example.h
class Example {
public:
// Public function has no locking requirements and thus needs no annotation.
int IncreaseFoo(int by);
private:
// This is an internal helper routine that can only be called with |lock_|
// held. Calling this without holding |lock_| is a compile-time error.
// Annotations like TA_REQ, TA_ACQ, TA_REL, etc are part of the function's
// interface and must be on the function declaration, usually in the header,
// not the definition.
int IncreaseFooLocked(int by) TA_REQ(lock_);
// This internal routine requires that both |lock_| and |foo_lock_| be held by the
// caller.
int IncreaseFooAndBarLocked(int foo_by, int bar_by) TA_REQ(lock_) TA_REQ(bar_lock_);
// The TA_GUARDED(lock_) annotation on |foo_| means that |lock_| must be
// held to read or write from |foo_|.
int foo_ TA_GUARDED(lock_);
// |lock_| can be declared after annotations referencing it,
// if desired.
Mutex lock_;
Mutex bar_lock_;
};
// example.cpp
int Example::IncreaseFoo(int by) {
int new_value;
{
AutoLock lock(&lock_); // fbl::AutoLock is annotated
new_value = IncreaseFooLocked(by);
}
return new_value;
}
Note that for annotations, which allow sets of mutex objects, one may either apply the annotation multiple times, or provided a comma separated list to the annotation. In other words, the following two declarations are equivalent.
int IncreaseFooAndBarLocked(int foo_by, int bar_by) TA_REQ(lock_) TA_REQ(bar_lock_);
int IncreaseFooAndBarLocked(int foo_by, int bar_by) TA_REQ(lock_, bar_lock_);
Library code exposed through the sysroot must use the more awkwardly named macros provided by system/public/zircon/compiler.h to avoid collisions with consumers of the sysroot.
Annotations should complement the comments and identifiers to make the code understandable. Annotations do not replace comments or clear names. Try to follow these best practices when writing code involving locking:
-
Group member variables protected by a lock with the lock. Where it makes sense, document what is protected by what with a comment in addition to the annotations. For example when several member variables are protected by one lock and several are protected by a different lock, a comment is easier to read than going through each annotation.
-
Name functions that require a lock be held with a 'Locked()' suffix. If there are multiple locks that could be plausibly held to call the function, consider making the choice clear in the function name. Keep in mind readers of calling code will not be able to see the annotations.
The thread safety analysis is a purely static check done at compile time and
cannot understand conditionally held locks or locking patterns that span
compilation units in ways not expressible via static annotations. In many
situations, this analysis is still useful but there are situations that the
analysis simply cannot understand. The main escape hatch for disabling analysis
is to add the annotation TA_NO_THREAD_SAFETY_ANALYSIS
to the function definition
containing the code the analysis is confused by. Other escape mechanisms are
available as well - see the Clang documentation for details. Situations that
require disabling the analysis are likely to be complex for humans to understand
as well as machines and should be accompanied by a comment indicating the
invariants in use.
The thread safety analysis can be defeated in a number of ways, for instance
when using pointers. For example, when taking the address of a guarded data
member Clang loses track of the guard, e.g. for a foo_ protected by a lock_
a call to memset(&foo_, 0, sizeof(foo_))
without holding lock_ won't be caught
as a violation.