首页
/ OBS Studio libobs 跨平台线程同步 API 详解:os_event、os_sem 与原子操作(threading.h)

OBS Studio libobs 跨平台线程同步 API 详解:os_event、os_sem 与原子操作(threading.h)

2026-09-04 20:22:45作者:冯爽妲Honey

本文基于 OBS Studio 官方 Sphinx 参考文档 reference-libobs-util-threading.rst 编写,系统讲解 libobs/util/threading.h 提供的跨平台线程辅助设施:事件对象(os_event_t)、信号量(os_sem_t)与一组原子内联函数。读完本文,你不仅能完整掌握这些 API 的参数、返回值与使用方式,还能看懂它们在 POSIX 与 Windows 两个平台实现中的底层映射,以及它们在 OBS 主工程输出线程、热键轮询、GPU 编码限流等场景中的真实用法,从而在插件开发中写出可移植的同步代码。

一、threading.h 头文件定位与使用方式

libobs 为线程同步提供了一批辅助函数与类型。该模块的核心价值有两点:

  1. 跨平台统一接口:同一套 os_event_* / os_sem_* 函数在 Windows、macOS、Linux、FreeBSD 上行为一致,调用方无需写 #ifdef _WIN32 分支;
  2. Windows 上也能使用 pthread:文档明确说明 “The threading header will additionally provide access to pthread functions even on Windows”。从源码看,threading.h 无条件 #include <pthread.h>,再根据平台分别包含 threading-windows.hthreading-posix.h。仓库中 deps/w32-pthreads/ 目录自带了一份完整的 win32 pthread 移植实现(含 pthread.c、条件变量、互斥量、信号量等全套源文件与 CMake 构建脚本),为 Windows 构建提供 pthread API 基础。

使用方式只需一行:

#include <util/threading.h>

头文件还额外提供了两个内联小工具,方便处理 pthread 互斥量(见 threading.h):

  • pthread_mutex_init_value():由于 PTHREAD_MUTEX_INITIALIZER 只能在静态初始化时使用,该函数通过结构体赋值拷贝的方式在运行时给 pthread_mutex_t 写入静态初值;
  • pthread_mutex_init_recursive():封装了 pthread_mutexattr_settype(PTHREAD_MUTEX_RECURSIVE),初始化可重入互斥量。

二、线程类型:os_event_t 与 os_sem_t

文档将 os_event_tos_sem_t 列为本模块的两个线程类型。从 threading.h 看,二者都是不透明类型(opaque types),由前向声明的结构体 struct os_event_data / struct os_sem_data 定义,调用方只持有指针:

struct os_event_data;
struct os_sem_data;
typedef struct os_event_data os_event_t;
typedef struct os_sem_data os_sem_t;

两个平台的实际载体差异较大:

  • POSIX 平台os_event_data 是一个包含 pthread_mutex_tpthread_cond_tvolatile bool signalledbool manual 四个成员的结构体(见 threading-posix.c),即 libobs 用 pthread 条件变量自行模拟了“事件”这一 Windows 原生概念,这正是头文件注释所说的 “custom platform-independent 'event' handler via pthread conditional waits”;
  • Windows 平台os_event_t 本质上直接就是 CreateEvent 返回的内核句柄(*event = (os_event_t *)handle,见 threading-windows.c),零开销映射。

三、通用线程函数:os_set_thread_name

void os_set_thread_name(const char *name);

设置当前线程的名字,便于在调试器和性能分析器中识别线程。该函数在 threading.h 声明,各平台实现细节值得注意:

平台 实现方式 限制与细节
Apple pthread_setname_np(name) 直接透传
FreeBSD pthread_set_name_np(pthread_self(), name) 需显式传入线程句柄
glibc(Linux/Mingw) pthread_setname_np(pthread_self(), name) 线程名最长 15 字符;超长时源码会先 bstrdup_n(name, 15) 截断再设置,见 threading-posix.c
Windows 双通道:先用 VS 风格的 RaiseException(0x406D1388, ...) 注入调试器可见线程名,再动态加载 KernelBase.dll 调用 SetThreadDescription(Win10+ 标准 API) MinGW 下 __try 不可用时以空实现兜底;MSVC 风格实现见 threading-windows.c

四、事件函数:os_event 系列

事件(event)是“一个线程通知另一个/多个线程”的同步原语。libobs 将其抽象为 6 个函数:os_event_initos_event_destroyos_event_waitos_event_timedwaitos_event_tryos_event_signalos_event_reset

4.1 创建与销毁:os_event_init / os_event_destroy

int  os_event_init(os_event_t **event, enum os_event_type type);
void os_event_destroy(os_event_t *event);
  • event:输出参数,接收新事件对象的指针;
  • type:事件类型,定义于 threading.h,只有两种:
类型 语义
OS_EVENT_TYPE_AUTO 自动复位:被等待者取走后自动回到未信号状态(“谁醒来谁负责复位”)
OS_EVENT_TYPE_MANUAL 手动复位:保持信号状态,直到显式调用 os_event_reset(),多个等待者可同时被唤醒
  • 返回值:成功返回 0,失败返回负值。

POSIX 实现threading-posix.c):os_event_initbzalloc 分配结构体,再依次初始化互斥量与条件变量,任一步失败都会回滚已创建的资源并返回错误码;os_event_destroy 对 NULL 指针安全。

Windows 实现threading-windows.c):os_event_init 内部即 CreateEvent(NULL, (type == OS_EVENT_TYPE_MANUAL), FALSE, NULL)——第二个参数就是自动/手动复位标志,初始状态为非信号;os_event_destroy 对应 CloseHandle

4.2 等待:os_event_wait / os_event_timedwait / os_event_try

int os_event_wait(os_event_t *event);
int os_event_timedwait(os_event_t *event, unsigned long milliseconds);
int os_event_try(os_event_t *event);

os_event_wait:阻塞等待事件进入信号状态,成功返回 0,否则返回负值。POSIX 实现是一个经典的“while + 条件变量”循环(threading-posix.c):

pthread_mutex_lock(&event->mutex);
while (!event->signalled) {
    code = pthread_cond_wait(&event->cond, &event->mutex);
    if (code != 0)
        break;
}
if (code == 0) {
    if (!event->manual)
        event->signalled = false;   /* AUTO 类型:唤醒者自动复位 */
}
pthread_mutex_unlock(&event->mutex);

两个关键细节:一是循环等待,用于防御虚假唤醒(spurious wakeup);二是 os_event_wait 在 AUTO 模式下由“第一个醒来的等待者”清除 signalled 标志,这正是自动复位语义的 POSIX 等价实现。

os_event_timedwait:带超时的等待,milliseconds 为最长等待毫秒数,返回值是文档中列出的三态:

返回值 含义
0 成功(事件在超时前被信号化)
ETIMEDOUT 等待超时
EINVAL 发生预期之外的错误

POSIX 实现中,截止时间由 clock_gettime(CLOCK_REALTIME, ...)add_ms_to_ts() 换算得到(Apple 与 MinGW 环境回退到 gettimeofday),再交给 pthread_cond_timedwaitthreading-posix.c)。Windows 实现则直接把毫秒数传给 WaitForSingleObject,将 WAIT_TIMEOUT 映射为 ETIMEDOUT、其余异常映射为 EINVALthreading-windows.c)。

os_event_try:不等待,只探测事件是否处于信号状态:

返回值 含义
0 事件已信号化(且 AUTO 模式下随即被复位)
EAGAIN 事件未信号化
EINVAL 发生预期之外的错误

POSIX 实现即加锁检查 signalled 标志(threading-posix.c);Windows 实现是 WaitForSingleObject(handle, 0) 的零超时探测。

4.3 信号与复位:os_event_signal / os_event_reset

int  os_event_signal(os_event_t *event);
void os_event_reset(os_event_t *event);
  • os_event_signal:将事件置为信号状态并唤醒等待者,成功返回 0,否则负值。POSIX 实现在同一把锁内完成 pthread_cond_signal + signalled = truethreading-posix.c);Windows 即 SetEvent
  • os_event_reset:清除信号状态。对 MANUAL 事件这是唯一的复位手段;对 AUTO 事件一般无需调用(等待者已自动复位)。POSIX 实现加锁后清 signalled 标志;Windows 即 ResetEvent

AUTO 与 MANUAL 的选型:从 OBS 主工程的用法可以总结出清晰的分界线——“一次性通知/工作就绪”用 AUTO(如缓冲区空间释放、新数据到达),“运行期开关/停止标志”用 MANUAL(如输出停止标志、GPU 编码停用标志),后文第七节有真实代码佐证。

五、信号量函数:os_sem 系列

int  os_sem_init(os_sem_t **sem, int value);
void os_sem_destroy(os_sem_t *sem);
int  os_sem_post(os_sem_t *sem);
int  os_sem_wait(os_sem_t *sem);
  • os_sem_init:创建信号量,value 为初始计数值,成功返回 0,失败返回负值;
  • os_sem_destroy:销毁对象,对 NULL 指针安全;
  • os_sem_post:计数加一(若计数为 0 则唤醒一个等待者);
  • os_sem_wait:计数减一,若计数已为 0 则阻塞直到被加一。

三个平台的底层载体各不相同:

平台 底层实现 源码位置
Linux/FreeBSD 进程内信号量 sem_tsem_init(&new_sem, 0, value)(第二参数 0 表示不跨进程共享) threading-posix.c
macOS Mach 语义信号量 semaphore_create(task, ...),销毁时 semaphore_destroy threading-posix.c
Windows 内核对象 CreateSemaphore(NULL, value, 0x7FFFFFFF, NULL)post/wait 对应 ReleaseSemaphore/WaitForSingleObject threading-windows.c

os_sem_wait 的无限阻塞特性(POSIX sem_wait、Windows INFINITE)意味着调用前必须保证对端有对应的 post,否则线程将永久挂起——这一点在写插件时尤其需要留意。

六、原子内联函数:os_atomic 系列

文档列出的原子函数全部是 static inline 内联函数,按平台分两套实现:POSIX 系基于 GCC/Clang 的 __atomic 内建(memory order 均为 __ATOMIC_SEQ_CST 顺序一致性),Windows 系基于 MSVC 的 Interlocked 内建。

6.1 long 类型原子操作

函数 语义 POSIX 实现 Windows 实现
long os_atomic_inc_long(volatile long *val) 原子加 1,返回新值 __atomic_add_fetch(val, 1, SEQ_CST) _InterlockedIncrement(val)
long os_atomic_dec_long(volatile long *val) 原子减 1,返回新值 __atomic_sub_fetch(val, 1, SEQ_CST) _InterlockedDecrement(val)
void os_atomic_store_long(volatile long *ptr, long val) 原子写入 __atomic_store_n x86/x64 用 _InterlockedExchange;ARM64 用 __stlr32 + 读写屏障(见 threading-windows.h
long os_atomic_set_long(volatile long *ptr, long val) 交换并返回旧值(文档注明 “Badly named”) __atomic_exchange_n _InterlockedExchange
long os_atomic_exchange_long(volatile long *ptr, long val) 交换并返回旧值(“Properly named”) os_atomic_set_long 同一实现 同左
long os_atomic_load_long(const volatile long *ptr) 原子读取 __atomic_load_n __iso_volatile_load32/ARM64 __ldar32 + 屏障
bool os_atomic_compare_swap_long(volatile long *val, long old_val, long new_val) CAS:当前值等于 old_val 时才写入 new_val,返回是否成功 __atomic_compare_exchange_n _InterlockedCompareExchange

一个值得提醒的命名陷阱os_atomic_set_long / os_atomic_set_bool 虽然名字叫 “set”,实际语义是 exchange(交换并返回旧值)——文档原文特意标注了 “Badly named”,而 os_atomic_exchange_* 才是命名正确的等价物。二者在两个平台的实现中就是同一个函数(threading-posix.hthreading-windows.h),这是历史兼容遗留。

另外,头文件中还提供了文档未列出的出参版本 os_atomic_compare_exchange_long(volatile long *val, long *old_val, long new_val)threading-posix.hthreading-windows.h),CAS 失败时会把实际读到的当前值写回 *old_val,适合实现自旋重试循环。

6.2 bool 类型原子操作

函数 语义
void os_atomic_store_bool(volatile bool *ptr, bool val) 原子写入布尔值
bool os_atomic_set_bool(volatile bool *ptr, bool val) 交换并返回旧值(“Badly named”)
bool os_atomic_exchange_bool(volatile bool *ptr, bool val) 交换并返回旧值(“Properly named”)
bool os_atomic_load_bool(const volatile bool *ptr) 原子读取布尔值

Windows 实现里 os_atomic_load_bool / os_atomic_set_bool 特意用 memcpychar 拷贝到 bool,源码注释说明原因:“Avoid unnecessary char to bool conversion. Value known 0 or 1.”(threading-windows.h);ARM64 分支则使用 __ldar8 / __stlr8 带释放/获取语义的指令保证可见性顺序。

七、OBS 主工程中的真实用法

以下示例全部来自仓库源码,展示了 threading.h 各 API 在生产代码中的典型组合方式。

1. 输出线程的“可中断等待”——MANUAL 事件 + timedwait(输出重连场景)

obs-output.c 中为每个输出创建 stopping_eventOS_EVENT_TYPE_MANUAL);重连逻辑(obs-output.c)则用“最多等 N 毫秒、被提前信号化则立刻返回”的惯用法:

if (os_event_timedwait(output->reconnect_stop_event, output->reconnect_retry_cur_msec) == ETIMEDOUT)

ETIMEDOUT 表示等待期内未被停止,继续执行重连;等待提前返回则说明收到停止信号,退出重试循环。

2. 热键轮询循环——timedwait 作短周期 tick

obs-hotkey.c 的热键线程以 25 毫秒为一个节拍轮询:

while (os_event_timedwait(obs->hotkeys.stop_event, 25) == ETIMEDOUT) {

只有超时(未收到停止请求)才继续下一轮轮询,收到停止信号则自然退出循环——这是一个不需要忙等、又能在数毫秒内响应退出的标准写法。

3. 视频帧与 GPU 编码的背压控制——信号量

video-io.c 在视频输出对象上创建 update_semaphore(初值 0);GPU 编码路径则同时使用信号量与 MANUAL 事件配对(obs-video-gpu-encode.c):

os_sem_init(&video->gpu_encode_semaphore, 0);
os_event_init(&video->gpu_encode_inactive, OS_EVENT_TYPE_MANUAL);

“信号量控制吞吐、事件表达开关状态”是这里明确的分工。

4. 任务队列——信号量 + AUTO 事件组合

通用任务队列 task.c 同时创建初值为 0 的 os_sem_t 和一个 AUTO 型 wait_event,前者用于限制并发/传递工作项,后者用于“有任务到达”的一次性通知。

5. 缓冲文件写入的产消同步——双 AUTO 事件

buffered-file-serializer.c 中同时创建两个 AUTO 事件:

os_event_init(&out->io.buffer_space_available_event, OS_EVENT_TYPE_AUTO);
os_event_init(&out->io.new_data_available_event, OS_EVENT_TYPE_AUTO);

分别表示“缓冲区有空间可写”与“有新数据可读”,是两个方向的单发通知,典型的一次性事件语义。

6. 无锁引用计数——原子递减判零释放

libobs 大量核心对象的释放依赖 os_atomic_dec_long(...) 返回 0 的判零模式,例如回调信号处理器的引用计数(signal.c)与数据对象引用计数(obs-data.c):

if (handler && os_atomic_dec_long(&handler->refs) == 0) { /* 最后一个引用者负责释放 */ }

视频对象的 GPU 引用计数(video-io.c)甚至把原子递减与布尔原子读取组合成复合条件 os_atomic_dec_long(&video->gpu_refs) == 0 && !os_atomic_load_bool(&video->raw_active),体现了原子操作与状态标志配合使用的方式。

八、返回值约定速查与使用要点

综合文档与两套平台实现,本模块的返回值约定高度统一:

场景 返回值
一切成功(init/wait/post/signal 等) 0
初始化/系统调用失败 负值(POSIX 下通常透传底层错误码)
os_event_timedwait 超时 ETIMEDOUT
os_event_try 未信号化 EAGAIN
os_event_timedwait / os_event_try 意外错误 EINVAL

使用要点小结:

  1. 生命周期:所有对象遵循 init → 使用 → destroy 的配对纪律,destroy 系列与 post/wait 系列都对 NULL 做了防护,但 os_event_wait / signal 等对空句柄直接返回错误码,调用方仍应避免悬挂使用;
  2. AUTO vs MANUAL:一次性通知选 AUTO(等待者自动复位),全局开关选 MANUAL(需显式 os_event_reset);
  3. 跨线程命名:在线程函数入口调用 os_set_thread_name,注意 glibc 的 15 字符限制;
  4. 原子操作命名:需要“交换取旧值”时优先使用 os_atomic_exchange_* 这组命名正确的函数,避免被 os_atomic_set_* 的旧名误导;
  5. 可移植性:只要依赖 util/threading.h 的这组 API 而不直接调用平台同步原语,代码即可在 Windows、macOS、Linux 与 FreeBSD 上无差别编译运行——这正是 libobs 将其作为插件开发推荐同步设施的原因。

本文全部 API 语义均继承自 reference-libobs-util-threading.rst,实现细节以 threading.hthreading-posix.cthreading-windows.c 及对应平台原子操作头文件为准,可结合 reference-libobs-util.rst 继续查阅 libobs 工具库的其他模块。

登录后查看全文
热门项目推荐
相关项目推荐