在上一篇文章中,我们拆解了 butex——bthread 的 futex 同步原语。我们看到 bthread_mutex_t 内部持有一个 butex 指针,bthread_sem_t 也是如此。butex 是同步的"引擎",而这些类型是面向用户的"方向盘"。
本文聚焦 bthread/types.h,拆解 bthread 的完整类型系统。这是一个"pthread 的平行宇宙"——每个 pthread 类型都有对应的 bthread 版本,外观相似,内部机制却截然不同。
一、M:N 模型与类型设计
bthread 是 M:N 协程库:M 个 bthread 复用到 N 个 pthread 上运行。这决定了类型设计的核心原则:
| | |
|---|
| | |
| | |
| | bthread 的同步基于 butex(参见本系列第四篇) |
从类型层面看,bthread 提供了 pthread 的完整镜像:
pthread_t → bthread_t // 线程 IDpthread_attr_t → bthread_attr_t // 创建属性pthread_mutex_t → bthread_mutex_t // 互斥锁pthread_cond_t → bthread_cond_t // 条件变量sem_t → bthread_sem_t // 信号量pthread_rwlock_t → bthread_rwlock_t // 读写锁pthread_barrier_t → bthread_barrier_t // 栅栏pthread_once_t → bthread_once_t // 一次性初始化pthread_key_t → bthread_key_t // 线程本地存储键
每个类型都做了两件事:对外提供与 pthread 相似的接口,对内使用 butex 实现协程友好的等待/唤醒。
二、bthread_t:协程 ID
typedef uint64_t bthread_t;static const bthread_t INVALID_BTHREAD = 0;
bthread_t 是一个 64 位无符号整数,唯一标识一个 bthread。约定:非零值为有效 bthread,零值(INVALID_BTHREAD)表示无效。
在 butex 的等待者中,我们也见过这个约定——ButexWaiter::tid 为 0 表示 pthread 等待者,非零表示 bthread 等待者。这里 0 是 pthread 的"领地",非零是 bthread 的"领地"。
三、bthread_attr_t:创建属性
bthread_attr_t 是最复杂的类型定义,控制 bthread 的创建行为。
3.1 结构体定义
typedef struct bthread_attr_t { bthread_stacktype_t stack_type; // 栈类型 bthread_attrflags_t flags; // 属性标志位 bthread_keytable_pool_t* keytable_pool; // TLS 内存池(NULL = 默认) bthread_tag_t tag; // 标签(用于调度隔离) char name[BTHREAD_NAME_MAX_LENGTH + 1]; // 名称(调试用)} bthread_attr_t;

四个维度控制 bthread 的行为:
3.2 栈类型(stack_type)
BTHREAD_STACKTYPE_PTHREAD = 1 // 直接用 pthread 栈(用于 JNI)BTHREAD_STACKTYPE_SMALL = 2 // 小栈(约 32KB)BTHREAD_STACKTYPE_NORMAL = 3 // 标准栈(默认,约 1MB)BTHREAD_STACKTYPE_LARGE = 4 // 大栈(约 8MB)
为什么需要 STACKTYPE_PTHREAD?有些代码(如 JNI)会检查栈布局,必须在真实的 pthread 栈上运行。代价是这种 bthread 阻塞时真正阻塞底层 pthread,需要更多的 worker 线程。
3.3 属性标志位(flags)
BTHREAD_LOG_START_AND_FINISH = 8 // 记录启动/结束日志BTHREAD_LOG_CONTEXT_SWITCH = 16 // 记录上下文切换日志BTHREAD_NOSIGNAL = 32 // 不立即唤醒,配合 bthread_flush 批量创建BTHREAD_NEVER_QUIT = 64 // 禁止退出标志BTHREAD_INHERIT_SPAN = 128 // 继承父 bthread 的追踪 SpanBTHREAD_GLOBAL_PRIORITY = 256 // 使用全局优先级调度
标志位的设计很巧妙:低 3 位保留给栈类型,高位才是标志。这意味着可以把栈类型和标志编码到一个 unsigned 中:
// 低 3 位 = 栈类型,高位 = 标志void operator=(unsigned stacktype_and_flags) { stack_type = (stacktype_and_flags & 7); flags = (stacktype_and_flags & ~(unsigned)7u); keytable_pool = NULL; tag = BTHREAD_TAG_INVALID;}
还支持 | 运算符组合属性:
bthread_attr_t attr = BTHREAD_ATTR_NORMAL | BTHREAD_NOSIGNAL;
3.4 标签(tag)
typedef int bthread_tag_t;static const bthread_tag_t BTHREAD_TAG_INVALID = -1;static const bthread_tag_t BTHREAD_TAG_DEFAULT = 0;
标签用于调度隔离——不同 tag 的 bthread 在不同的 TaskGroup 中运行,互不干扰。这在get_task_group 中已经体现:唤醒时根据 tag 选择对应的 TaskGroup。
3.5 预定义属性
brpc 提供了常用的预定义属性,覆盖大多数场景:
BTHREAD_ATTR_PTHREAD // 运行在 pthread 栈上(JNI 场景)BTHREAD_ATTR_SMALL // 小栈,短生命周期任务BTHREAD_ATTR_NORMAL // 标准栈,默认选择BTHREAD_ATTR_LARGE // 大栈,深递归场景BTHREAD_ATTR_DEBUG // 调试属性,记录启动/切换/结束日志
四、同步原语类型
这些类型是 butex(本系列第四篇)的直接使用者——每个同步原语内部都持有一个或多个 butex 指针。
4.1 bthread_mutex_t:互斥锁
typedef struct bthread_mutex_t { unsigned* butex; // 核心:指向一个 butex bthread_contention_site_t csite; // 竞争点统计 bool enable_csite; // 是否启用统计 mutex_owner_t owner; // 持有者信息(仅调试模式)} bthread_mutex_t;

butex 是锁的核心——0 表示未锁定,1 表示已锁定。加锁时通过 butex_wait 等待,解锁时通过 butex_wake 唤醒。因为 butex 同时支持 bthread 和 pthread,bthread_mutex_t 天然支持跨线程类型使用。
csite(contention site)用于锁竞争的性能分析——记录等待时间,帮助定位热点。owner 字段仅在 BRPC_DEBUG_LOCK=1 时启用,会带来约 50% 的性能开销,仅用于调试。
4.2 bthread_cond_t:条件变量
typedef struct bthread_cond_t { bthread_mutex_t* m; // 关联的互斥锁 int* seq; // 序列号 butex} bthread_cond_t;

条件变量需要两个东西:一个互斥锁 m(保护条件谓词)和一个序列号 butex seq(实现 wait/signal 同步)。每次 notify 时递增序列号,等待者通过比较序列号判断是否被唤醒。
这里 seq 是 int* 类型——它就是 butex_create() 返回的 int*(butex 的 value 地址)。
4.3 bthread_sem_t:信号量
typedef struct bthread_sem_t { unsigned* butex; // butex 指针,存储信号量计数值 bool enable_csite; // 是否启用竞争统计} bthread_sem_t;

信号量的实现最为直接:*butex 就是信号量的计数值。wait 时减 1,减到 0 就阻塞在 butex 上;post 时加 1 并唤醒。
4.4 bthread_rwlock_t:读写锁(最复杂)
typedef struct bthread_rwlock_t { bthread_sem_t reader_sema; // 读者等待写者完成 bthread_sem_t writer_sema; // 写者等待读者完成 int reader_count; // 活跃读者计数 int reader_wait; // 正在等待离开的读者数 bool wlock_flag; // 写锁持有标志 bthread_mutex_t write_queue_mutex; // 写者队列互斥锁 bthread_contention_site_t writer_csite; // 写者竞争统计} bthread_rwlock_t;

读写锁是组合最复杂的类型——它用两个信号量 + 一个互斥锁 组合实现:
reader_semawriter_semawrite_queue_mutex:有等待写者时持有,阻止新读者进入(写者优先策略)
写者优先的含义:一旦有写者开始等待,新的读者会被 write_queue_mutex 挡住,确保写者不会饿死。
4.5 bthread_barrier_t:栅栏
typedef struct { unsigned int count; // 需要同步的 bthread 数量} bthread_barrier_t;
栅栏的实现最简洁——只需要一个计数器。所有 bthread 到达后一起继续执行。
4.6 bthread_once_t:一次性初始化
classbthread_once_t{public: enum State { UNINITIALIZED = 0, // 未初始化 INPROGRESS, // 正在初始化 INITIALIZED, // 已完成 };private: butil::atomic<int>* _butex; // butex 存储状态};
bthread_once_t 用 butex 实现状态机:UNINITIALIZED → INPROGRESS → INITIALIZED。第一个调用者将状态改为 INPROGRESS 并执行初始化函数,其他调用者在 butex 上等待。初始化完成后设为 INITIALIZED 并唤醒所有等待者。
五、线程本地存储类型
5.1 bthread_key_t:TLS 键
typedef struct { uint32_t index; // KeyTable 中的索引 uint32_t version; // 版本号(ABA 问题防护)} bthread_key_t;
与 pthread_key_t 类似,但增加了 version 字段。为什么?因为键的索引可能被复用——一个键被删除后,新的键可能分配到相同的 index。version 每次递增,即使 index 相同,version 不同的键也被视为不同的键,避免 ABA 问题。
5.2 bthread_keytable_pool_t:KeyTable 内存池
typedef struct { pthread_rwlock_t rwlock; // 保护并发访问 void* list; // 键值对列表 void* free_keytables; // 空闲 KeyTable 链表(复用) size_t size; // 池容量 int destroyed; // 销毁标志} bthread_keytable_pool_t;
每个正在运行的 bthread 都需要一个 KeyTable 来存储线程私有数据。内存池批量管理和复用 KeyTable,减少频繁分配/释放的开销。
注意这里用的是 pthread_rwlock_t 而非 bthread_rwlock_t——KeyTable 池的管理发生在 pthread 层面,不需要协程友好的锁。
六、辅助类型与全局常量
6.1 bthread_id_t:资源标识符
typedef struct { uint64_t value;} bthread_id_t;static const bthread_id_t INVALID_BTHREAD_ID = {0};
bthread_id_t 与 bthread_t 是两套独立的标识体系。bthread_t 标识协程(由调度器分配),bthread_id_t 标识资源(由 ResourcePool 分配)。bthread_id_t 主要用于 RPC 框架中的请求-响应匹配。
注意不要混淆 INVALID_BTHREAD(bthread_t = 0)和 INVALID_BTHREAD_ID(bthread_id_t.value = 0)——它们属于不同的命名空间。
6.2 全局常量
BTHREAD_EPOLL_THREAD_NUM = 1 // epoll 线程数(固定 1 个)BTHREAD_MIN_CONCURRENCY = 4 // 最小 worker 数(3 + 1 epoll)BTHREAD_MAX_CONCURRENCY = 1024 // 最大 worker 数BTHREAD_MIN_PARKINGLOT = 4 // 最小 ParkingLot 数BTHREAD_MAX_PARKINGLOT = 1024 // 最大 ParkingLot 数
这些常量定义了调度器的边界。BTHREAD_MIN_CONCURRENCY = 4 意味着即使负载很低,也至少有 3 个 worker 线程 + 1 个 epoll 线程在运行。
6.3 bthread_contention_site_t:竞争统计
typedef struct { int64_t duration_ns; // 等待时间(纳秒) size_t sampling_range; // 采样范围} bthread_contention_site_t;
被 bthread_mutex_t 和 bthread_rwlock_t 使用,用于锁竞争的性能分析。记录在同步原语上的等待时间分布,帮助定位锁竞争热点。
七、类型间的依赖关系
这些类型不是孤立的——它们构成了一个层次分明的依赖图:
bthread_attr_t(创建属性) └─ stack_type + flags + tag + keytable_pool同步原语层次: butex(32 位原子,本系列第四篇) ├─ bthread_mutex_t(1 个 butex) │ └─ bthread_rwlock_t(2 个 sem + 1 个 mutex) ├─ bthread_sem_t(1 个 butex) ├─ bthread_cond_t(1 个 mutex + 1 个 seq butex) └─ bthread_once_t(1 个 butex,状态机)TLS 层次: bthread_key_t(index + version) └─ bthread_keytable_pool_t(管理 KeyTable)
关键观察:butex 是唯一的底层原语,所有同步类型都建立在 butex 之上。这也是为什么 butex 需要同时支持 bthread 和 pthread——上层的互斥锁、信号量可能在任何上下文中被使用。
八、总结
bthread 的类型系统可以归纳为一句话:对外镜像 pthread 接口,对内统一使用 butex 实现。
三个设计特点:
1. 完整的 pthread 镜像。 从线程 ID 到互斥锁、条件变量、信号量、读写锁、栅栏、一次性初始化,每个 pthread 类型都有对应的 bthread 版本。用户可以像使用 pthread 一样使用 bthread,无需学习新的 API。
2. butex 作为统一底层。bthread_mutex_t 持有 butex 指针,bthread_sem_t 持有 butex 指针,bthread_cond_t 持有 seq butex,bthread_once_t 用 butex 实现状态机。所有同步原语共享同一个底层机制,保持了一致的行为和性能特征。
3. 组合式设计。bthread_rwlock_t 由两个 bthread_sem_t 和一个 bthread_mutex_t 组合而成;bthread_cond_t 由一个 bthread_mutex_t 和一个 seq butex 组合。复杂的同步原语通过组合简单的原语构建,而不是从零实现。
理解了这套类型体系,后续文章拆解各个同步原语的实现时,就有了清晰的类型框架。
本文基于 Apache brpc 源码(src/bthread/types.h)撰写。