C++binary_semaphore二值信号量

2026-03-22 20:00:41 278阅读

C++20 中的 binary_semaphore:轻量级同步原语详解

在现代 C++ 并发编程中,信号量(semaphore)是实现线程间资源协调与访问控制的重要工具。C++20 标准首次将信号量引入 <semaphore> 头文件,其中 std::binary_semaphore 作为最基础、最高效的二值信号量实现,专为“开/关”型同步场景设计——其内部计数器仅允许取值 0 或 1,语义简洁、开销极低,特别适用于互斥保护、事件通知及生产者-消费者模型中的轻量同步。

binary_semaphore 并非 std::mutex 的替代品,而是一种更底层、更灵活的同步原语。它不绑定线程所有权,允许多个线程以非对称方式调用 acquire()release();它也不提供 RAII 封装(如 std::lock_guard),因此需开发者显式管理生命周期,但也正因如此,它避免了锁的递归检查、异常安全开销等额外负担,在高性能系统(如实时任务调度、无锁数据结构辅助同步)中展现出独特价值。

基本接口与语义约束

std::binary_semaphore 提供三个核心成员函数

  • binary_semaphore(bool desired = true):构造函数,初始状态由 desired 决定(true 表示计数为 1,可立即 acquire()false 表示计数为 0,首次 acquire() 将阻塞)。
  • void acquire():原子地将计数减 1;若当前为 0,则挂起当前线程,直至其他线程调用 release()
  • void release():原子地将计数加 1(仅限从 0 到 1,因二值特性,多次 release() 不会累积)。

注意:acquire()release() 均为无异常操作,且不保证公平性(即等待线程唤醒顺序未指定),但所有操作均满足顺序一致性(memory_order_seq_cst),确保跨线程观察到一致的状态变化。

典型应用场景示例

场景一:线程启动同步(Wait-for-Ready)

常用于主线程等待工作线程完成初始化后才开始发送任务。相比 std::condition_variable 配合 std::mutexbinary_semaphore 更简洁、无锁竞争开销。

#include <semaphore>
#include <thread>
#include <iostream>

std::binary_semaphore ready{false}; // 初始不可获取

void worker() {
    std::cout << "Worker: initializing...\n";
    // 模拟耗时初始化
    std::this_thread::sleep_for(std::chrono::milliseconds(200));
    std::cout << "Worker: ready!\n";
    ready.release(); // 通知主线程已就绪
}

int main() {
    std::thread t{worker};

    std::cout << "Main: waiting for worker...\n";
    ready.acquire(); // 阻塞直至 worker 调用 release()
    std::cout << "Main: proceeding with work.\n";

    t.join();
}

场景二:资源独占访问(轻量互斥)

当仅需保护一段短小临界区,且无需递归访问或条件等待时,binary_semaphore 可替代 std::mutex,减少系统调用与内核态切换。

#include <semaphore>
#include <thread>
#include <vector>
#include <iostream>

std::binary_semaphore guard{true}; // 初始可用
int shared_counter = 0;

void increment(int times) {
    for (int i = 0; i < times; ++i) {
        guard.acquire();     // 进入临界区
        ++shared_counter;    // 临界操作
        guard.release();     // 离开临界区
    }
}

int main() {
    const int N = 1000;
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i) {
        threads.emplace_back(increment, N);
    }
    for (auto& t : threads) {
        t.join();
    }
    std::cout << "Final counter: " << shared_counter << "\n"; // 输出 4000
}

场景三:生产者-消费者单槽缓冲

在仅需一个槽位的简化队列中,binary_semaphore 可分别表示“槽空”与“槽满”状态,配合原子变量实现零锁通信

#include <semaphore>
#include <thread>
#include <iostream>
#include <atomic>

std::binary_semaphore slot_empty{true};  // 初始为空
std::binary_semaphore slot_full{false};   // 初始为满(即无数据)
std::atomic<int> data{0};

void producer(int value) {
    slot_empty.acquire(); // 等待槽位空闲
    data.store(value, std::memory_order_relaxed);
    slot_full.release();  // 标记槽位已满
}

void consumer() {
    slot_full.acquire();  // 等待槽位有数据
    int val = data.load(std::memory_order_relaxed);
    std::cout << "Consumed: " << val << "\n";
    slot_empty.release(); // 标记槽位再次空闲
}

int main() {
    std::thread p{producer, 42};
    std::thread c{consumer};
    p.join();
    c.join();
}

注意事项与最佳实践

  • 勿重复释放:对已为 1 的 binary_semaphore 多次调用 release() 是未定义行为。应确保每次 release() 都对应一次成功的 acquire() 或明确的初始状态。
  • 避免死锁:务必配对使用 acquire()/release(),尤其在异常路径中。可借助 RAII 包装器(需自行实现)提升安全性。
  • 性能考量binary_semaphore 在多数实现中映射为 futex(Linux)或 SRWLOCK(Windows),争用时才陷入内核,因此高吞吐低争用场景下性能优异。
  • counting_semaphore<1> 的区别binary_semaphorecounting_semaphore<1> 的特化优化版本,标准要求其实现必须比通用计数信号量更高效(例如省略计数验证),故应优先选用 binary_semaphore

结语

std::binary_semaphore 是 C++20 并发工具箱中一枚精巧的“螺丝钉”——体积小、语义明、性能高。它不追求功能完备,而专注解决一类经典同步问题:二态协调。掌握其适用边界与使用范式,有助于开发者在复杂系统中做出更精准的同步原语选型,既避免过度设计,也规避潜在陷阱。在强调确定性延迟与资源效率的嵌入式、游戏引擎及高频交易系统中,这一轻量级同步机制正日益成为构建可靠并发逻辑的基石之一。

文章版权声明:除非注明,否则均为Dark零点博客原创文章,转载或复制请以超链接形式并注明出处。

目录[+]