Zircon 线程安全注解

Zircon 代码利用 Clang 的线程安全分析功能,对某些同步不变性进行文档和机器验证。在针对 Clang 进行构建时,系统会检查这些注解(如需了解如何使用 Clang 进行构建,请参阅使用入门)。

使用方法

Clang 文档

在 Zircon 中,我们自行提供了一组用于封装注解的宏,并为同步基元添加了注解。在编写涉及同步的新代码或为现有代码添加注释时,在大多数情况下,您应该使用 <lib/zircon-internal/thread_annotations.h 提供的线程注释宏。这些宏都以前缀 "TA_" 开头,用于进行线程分析。最常用的选项包括:

  • TA_GUARDED(x):带注解的变量由功能(例如锁)保护 x
  • TA_ACQ(x...) 函数会获取 x 集中的所有互斥量,并在返回后保留它们
  • TA_REL(x...) 函数会释放 x 集中的所有互斥量
  • TA_REQ(x...) 函数要求调用方持有 x 集中的所有互斥量
  • TA_EXCL(x...) 函数要求调用方不得持有 x 集内的任何互斥

例如,如果某个类包含受互斥保护的成员变量 'int foo_',系统会为其添加如下注解:

// 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;
}

请注意,对于允许多组互斥对象的注解,可以多次应用该注解,或为该注解提供一个以英文逗号分隔的列表。换句话说,以下两个声明是等效的。

    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_);

通过 sysroot 公开的库代码必须使用由 system/public/zircon/compiler.h 提供的命名更加复杂的宏,以避免与 sysroot 的使用方发生冲突。

最佳实践

注解应该对注解和标识符进行补充,使代码易于理解。注释不会替换注释或清除名称。编写涉及锁定的代码时,请尝试遵循以下最佳实践:

  • 由锁(带有锁)保护的群组成员变量。如果可以的话,除了注解之外,还应使用注释记录受哪些内容保护的内容。例如,当多个成员变量受一个锁保护,而多个成员变量受另一个锁保护时,注释的可读性比检查每个注释要容易。

  • 使用“Locked()”后缀命名需要锁的函数。如果有多个锁可能会合理地持有来调用该函数,请考虑在函数名称中明确说明这一选择。请注意,调用代码的读者不会看到这些注解。

限制

线程安全分析是在编译时完成的纯静态检查,无法理解有条件持有的锁或以无法通过静态注解表达的方式跨编译单元的锁定模式。在许多情况下,此分析仍然有用,但存在分析无法理解的情况。停用分析的主要方法是向函数定义添加注解 TA_NO_THREAD_SAFETY_ANALYSIS,其中包含混淆分析的代码。您也可以使用其他转义机制 - 如需了解详情,请参阅 Clang 文档。需要停用分析的情况对于人类和机器来说可能都很复杂,并且应该带有注释来指示正在使用的不变量。

线程安全分析可以通过多种方式失败,例如在使用指针时。例如,在获取受保护数据成员的地址时,Clang 无法追踪防护措施。例如,对于受锁保护的 foo_,在没有保留 lock 的情况下调用 memset(&foo_, 0, sizeof(foo_)) 不会被视为违规行为。