对锁定行为进行批注

若要避免多线程程序中的并发 Bug,请遵循适当的锁定规则并使用 SAL 批注。

并发 bug 很难重现、诊断和调试,因为它们是非确定性的。 有关线程交错的推理是最困难的,如果设计包含多个线程的代码体,这会变得不切实际。 因此,最好在多线程程序中遵循锁定规则。 例如,在获取多个锁时遵守锁定顺序可以帮助避免死锁,在访问共享资源之前获取适当的保护锁有助于避免争用条件。

遗憾的是,看似简单的锁定规则在实践中会很难遵循。 当今编程语言和编译器存在一个基本限制,即它们不能直接支持并发需求的规范和分析。 程序员必须依赖于非正式的代码批注来表示他们对于使用锁定的意图。

并发 SAL 批注用于帮助您指定锁定的副作用、锁定责任、数据保护、锁的顺序层次结构,以及其他预期的锁定行为。 通过将隐式规则设置为显式,SAL 并发批注可提供一致方式,用于说明代码使用锁定规则的方式。 并发批注还可增强代码分析工具查找争用条件、死锁、不匹配的同步操作和其他细微并发错误的能力。

一般性指导

通过使用注释,可以声明在实现(被调用方)和客户端(调用方)之间通过函数定义隐式使用的协定。 还可以表明程序中可进一步改进分析的固定条件及其他属性。

SAL 支持许多不同类型的锁定基元,例如临界区、互斥锁、自旋锁和其他资源对象。 许多并发批注采用锁表达式作为参数。 按照约定,锁由基础锁对象的路径表达式表示。

应记住的一些线程所有权规则:

  • 自旋锁是具有明确线程所有权的非计数锁。

  • 互斥锁和临界区是具有明确线程所有权的计数锁。

  • 信号灯和事件是不具有明确线程所有权的计数锁。

锁定批注

下表列出了锁定批注。

Annotation 说明
_Acquires_exclusive_lock_(expr) 批注函数并表明在状态后,函数会将 expr 命名的锁对象的排他锁计数递增 1。
_Acquires_lock_(expr) 批注函数并表明在状态后,函数会将 expr 命名的锁对象的锁计数递增 1。
_Acquires_nonreentrant_lock_(expr) 已获得由 expr 命名的锁。 如果已拥有此锁,会报告错误。
_Acquires_shared_lock_(expr) 批注函数并表明在状态后,函数会将 expr 命名的锁对象的共享锁计数递增 1。
_Create_lock_level_(name) 该语句声明符号 name 为锁级别,因此可以在批注 _Has_Lock_level__Lock_level_order_ 中使用。
_Has_lock_kind_(kind) 批注所有对象以优化资源对象的类型信息。 有时一种通用类型会用于不同类型的资源,并且重载的类型不足以区分各资源之间的语义需求。 下面提供了一个预定义 kind 参数的列表:

_Lock_kind_mutex_
互斥锁的锁类型 ID。

_Lock_kind_event_
事件的锁类型 ID。

_Lock_kind_semaphore_
信号量的锁类型 ID。

_Lock_kind_spin_lock_
自旋锁的锁类型 ID。

_Lock_kind_critical_section_
临界区的锁类型 ID。
_Has_lock_level_(name) 批注锁对象并赋予其 name 锁级别。
_Lock_level_order_(name1, name2) 该语句提供 name1name2 之间的锁排序。 具有 name1 级别的锁必须在具有 name2 级别的锁之前获得。
_Post_same_lock_(expr1, expr2) 批注函数并表明在状态后,两个锁 expr1expr2 被视为相同的锁对象。
_Releases_exclusive_lock_(expr) 批注函数并表明在状态后,函数会将 expr 命名的锁对象的排他锁计数递减 1。
_Releases_lock_(expr) 批注函数并表明在状态后,函数会将 expr 命名的锁对象的锁计数递减 1。
_Releases_nonreentrant_lock_(expr) 已发布 expr 命名的锁。 如果当前不持有此锁,会报告错误。
_Releases_shared_lock_(expr) 批注函数并表明在状态后,函数会将 expr 命名的锁对象的共享锁计数递减 1。
_Requires_lock_held_(expr) 批注函数并表明在状态前,expr 命名的对象的锁计数至少为 1。
_Requires_lock_not_held_(expr) 批注函数并表明在状态前,expr 命名的对象的锁计数为 0。
_Requires_no_locks_held_ 批注函数并表明检查器已知的所有锁的锁计数为 0。
_Requires_shared_lock_held_(expr) 批注函数并表明在状态前,expr 命名的对象的共享锁计数至少为 1。
_Requires_exclusive_lock_held_(expr) 批注函数并表明在状态前,expr 命名的对象的排他锁计数至少为 1。

非公开锁定对象的 SAL 内部

某些锁对象不通过关联的锁定函数的实现公开。 下表列出可对在未公开的锁对象上运行的函数启用批注的 SAL 内部变量。

Annotation 说明
_Global_cancel_spin_lock_ 说明取消自旋锁。
_Global_critical_region_ 说明临界区。
_Global_interlock_ 说明互锁操作。
_Global_priority_region_ 说明优先级区域。

共享数据访问批注

下表列出了用于共享数据访问的批注。

Annotation 说明
_Guarded_by_(expr) 批注变量并表明变量每次受到访问时,expr 命名的锁对象的锁计数至少为 1。
_Interlocked_ 批注变量,与 _Guarded_by_(_Global_interlock_) 等效。
_Interlocked_operand_ 批注的函数参数是各个互锁函数之一的目标操作数。 这些操作数必须具有特定的附加属性。
_Write_guarded_by_(expr) 批注变量并表明变量每次受到修改时,expr 命名的锁对象的锁计数至少为 1。

智能锁和 RAII 批注

智能锁通常包装本机锁并管理其生存期。 下表列出了可与智能锁和支持 move 语义的 RAII 编码模式一起使用的批注。

Annotation 说明
_Analysis_assume_smart_lock_acquired_(lock) 告知分析器假设已获取智能锁。 此批注需要引用锁类型作为其参数。
_Analysis_assume_smart_lock_released_(lock) 告知分析器假设已释放智能锁。 此批注需要引用锁类型作为其参数。
_Moves_lock_(target, source) 描述将锁状态从 source 对象转移到 targetmove constructor 操作。 target 被认为是一个新构造的对象,因此它之前的任何状态都会丢失并被 source 状态替换。 source 也会被重置为没有锁定计数或别名目标的干净状态,但指向它的别名保持不变。
_Replaces_lock_(target, source) 描述在从源传输状态之前释放目标锁的 move assignment operator 语义。 可将其看作是 _Moves_lock_(target, source) 前面有一个 _Releases_lock_(target) 的组合。
_Swaps_locks_(left, right) 描述假定对象 leftright 交换其状态的标准 swap 行为。 交换的状态包括锁计数和别名目标(如果存在)。 指向 leftright 对象的别名保持不变。
_Detaches_lock_(detached, lock) 描述锁包装器类型允许取消与其包含资源的关联的情况。 这类似于 std::unique_ptr 使用其内部指针的方式:它允许程序员提取指针并将其智能指针容器保持在干净状态。 std::unique_lock 支持类似的逻辑,并且可以在自定义锁包装器中实现。 分离锁保持其状态(锁计数和别名目标,如果有的话),而包装器被重置为包含零锁计数和没有别名目标,同时保留其自己的别名。 对锁计数没有任何操作(释放和获取)。 此注释的行为与 _Moves_lock_ 完全相同,只是分离的参数应该是 return 而不是 this

另请参阅