news 2026/5/9 19:57:32

C++ 信号量

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++ 信号量

C++ 信号量(Semaphore)详解与编程实践 信号量是多线程同步与互斥的核心工具之一,它能有效解决多线程间的资源竞争、任务协同问题。本文将从信号量的核心概念出发,逐步讲解其在 C++ 中的实现与使用,帮助你快速掌握这一关键技术。 一、信号量的核心概念 1. 什么是信号量? 信号量(Semaphore)本质上是一个计数器+等待 / 唤醒机制,用于控制多个线程对共享资源的访问权限,或实现线程间的有序协同。 它由荷兰计算机科学家 Dijkstra 于 1965 年提出,核心有两个操作(原子操作,不可被中断): P 操作(申请资源):也叫wait操作,将信号量计数器减 1。如果减 1 后计数器≥0,说明申请资源成功,线程继续执行;如果计数器<0,说明资源耗尽,线程被阻塞(进入等待队列),直到有其他线程释放资源。 V 操作(释放资源):也叫post操作,将信号量计数器加 1。如果加 1 后计数器≤0,说明有线程正在等待该资源,会唤醒一个等待队列中的线程,让其继续执行;如果计数器>0,说明没有线程等待,仅完成计数器更新。 2. 信号量的两种核心类型 (1) 互斥信号量(二进制信号量) 计数器的值只能是0或1,等价于一个 “互斥锁”,用于解决临界区资源的互斥访问(同一时间只有一个线程能访问共享资源)。 初始值为1,表示资源可用。 线程访问资源前执行 P 操作,计数器变为0(资源被占用)。 线程释放资源后执行 V 操作,计数器变回1(资源释放)。 如果有其他线程此时申请资源,会因计数器变为-1而被阻塞,直到持有资源的线程释放。 (2) 计数信号量(通用信号量) 计数器的值可以是任意非负整数,用于控制有限个共享资源的并发访问(同一时间允许 N 个线程访问共享资源)。 初始值为N(N 为共享资源的数量,例如线程池的最大并发数、缓冲区的容量)。 每个线程申请资源时执行 P 操作,计数器减 1;释放资源时执行 V 操作,计数器加 1。 当计数器变为0时,后续申请资源的线程会被阻塞,直到有线程释放资源。 二、C++ 中的信号量实现 C++ 标准库在C++20中才正式引入了信号量相关接口(位于<semaphore>头文件),在此之前,开发者通常使用第三方库(如 Boost)或操作系统提供的接口(如 Linux 的<semaphore.h>、Windows 的CreateSemaphore)。 本文将讲解两种常用实现: C++20 标准信号量(推荐,跨平台) Linux 系统信号量(兼容旧版本 C++,仅适用于 Linux/Unix) 前置说明 本文所有示例均基于多线程环境,需包含<thread>头文件,编译时需链接线程库(GCC 编译器添加-pthread参数)。 示例代码注重可读性,简化了部分异常处理,生产环境中需补充完善。 三、C++20 标准信号量实战 C++20 提供了两种标准信号量: std::counting_semaphore<>:计数信号量,模板参数为计数器的最大值(需为非负整数)。 std::binary_semaphore:二进制信号量,是std::counting_semaphore<1>的别名,等价于互斥信号量。 核心成员函数 函数 功能 对应操作 void acquire() 申请资源(P 操作),若资源不足则阻塞线程 P 操作 bool try_acquire() 尝试申请资源,成功返回true,失败返回false(不阻塞) 非阻塞 P 操作 void release(ptrdiff_t update = 1) 释放资源(V 操作),update为计数器增加的值(默认 1) V 操作 示例 1:二进制信号量实现互斥访问 需求:两个线程同时对同一个全局变量进行累加操作,使用std::binary_semaphore保证操作的原子性,避免数据竞争。 cpp 运行 #include <iostream> #include <thread> #include <semaphore> // C++20 标准信号量头文件 // 全局共享资源 int g_shared_count = 0; // 二进制信号量,初始值为1(资源可用) std::binary_semaphore g_binary_sem(1); // 累加任务函数 void increment_count(int times) { for (int i = 0; i < times; ++i) { // P操作:申请资源,进入临界区 g_binary_sem.acquire(); // 临界区:修改共享变量(原子操作,避免数据混乱) g_shared_count++; // 打印当前线程ID和累加后的值(仅用于演示) std::cout << "Thread " << std::this_thread::get_id() << " : g_shared_count = " << g_shared_count << std::endl; // V操作:释放资源,退出临界区 g_binary_sem.release(); } } int main() { // 创建两个线程,每个线程累加10次 std::thread t1(increment_count, 10); std::thread t2(increment_count, 10); // 等待两个线程执行完毕 t1.join(); t2.join(); // 打印最终结果 std::cout << "Final g_shared_count = " << g_shared_count << std::endl; return 0; } 编译与运行(GCC) bash 运行 # 需开启C++20标准,链接线程库 g++ -std=c++20 semaphore_binary.cpp -o semaphore_binary -pthread ./semaphore_binary 运行结果说明 两个线程不会同时修改g_shared_count,最终结果稳定为20,不会出现数据丢失(若不使用信号量,最终结果可能小于20)。 示例 2:计数信号量实现资源限流 需求:创建 5 个线程,模拟访问一个最多允许 2 个线程同时访问的共享资源,使用std::counting_semaphore实现限流。 cpp 运行 #include <iostream> #include <thread> #include <semaphore> #include <chrono> // 计数信号量,初始值为2(最多允许2个线程同时访问资源) std::counting_semaphore<10> g_counting_sem(2); // 最大值设为10,满足需求即可 // 资源访问任务函数 void access_resource(int thread_id) { // P操作:申请访问资源 g_counting_sem.acquire(); std::cout << "Thread " << thread_id << " : 成功获取资源,开始访问..." << std::endl; // 模拟资源访问耗时(休眠2秒) std::this_thread::sleep_for(std::chrono::seconds(2)); std::cout << "Thread " << thread_id << " : 资源访问完毕,释放资源..." << std::endl; // V操作:释放资源 g_counting_sem.release(); } int main() { const int thread_num = 5; std::thread threads[thread_num]; // 创建5个线程 for (int i = 0; i < thread_num; ++i) { threads[i] = std::thread(access_resource, i + 1); } // 等待所有线程执行完毕 for (int i = 0; i < thread_num; ++i) { threads[i].join(); } std::cout << "所有线程均完成资源访问" << std::endl; return 0; } 运行结果说明 运行后会发现,同一时间只有 2 个线程在访问资源,其余线程处于阻塞状态,直到有线程释放资源后,才会有新的线程获取资源并执行。 四、Linux 系统信号量(兼容旧版本 C++) 对于不支持 C++20 的环境,Linux/Unix 系统提供了<semaphore.h>头文件,实现了 POSIX 标准信号量,常用的有命名信号量(用于进程间通信)和无名信号量(用于线程间通信),本文重点讲解线程间通信的无名信号量。 核心函数 初始化信号量 c 运行 int sem_init(sem_t *sem, int pshared, unsigned int value); 参数 1:sem:指向要初始化的信号量对象。 参数 2:pshared:是否用于进程间共享,0表示仅用于线程间共享,非0表示用于进程间共享。 参数 3:value:信号量初始值(计数器初始值)。 返回值:成功返回0,失败返回-1。 P 操作(申请资源) c 运行 int sem_wait(sem_t *sem); 阻塞式申请资源,计数器减 1,资源不足则阻塞线程。 成功返回0,失败返回-1。 V 操作(释放资源) c 运行 int sem_post(sem_t *sem); 释放资源,计数器加 1,唤醒等待队列中的线程。 成功返回0,失败返回-1。 销毁信号量 c 运行 int sem_destroy(sem_t *sem); 释放信号量占用的资源,仅能销毁sem_init初始化的无名信号量。 成功返回0,失败返回-1。 示例:Linux 无名信号量实现线程协同 需求:实现 “生产者 - 消费者” 简单模型,生产者线程生产数据(存入全局变量),消费者线程消费数据,使用两个二进制信号量实现线程间协同。 cpp 运行 #include <iostream> #include <thread> #include <chrono> #include <semaphore.h> // Linux 信号量头文件 // 全局共享数据 int g_data = 0; // 两个二进制信号量 sem_t g_producer_sem; // 生产者信号量,初始值1(允许生产) sem_t g_consumer_sem; // 消费者信号量,初始值0(无数据可消费) // 生产者线程函数 void producer() { for (int i = 0; i < 5; ++i) { // P操作:申请生产权限 sem_wait(&g_producer_sem); // 生产数据 g_data = i + 1; std::cout << "生产者:生产数据 " << g_data << std::endl; // V操作:通知消费者可以消费 sem_post(&g_consumer_sem); // 模拟生产间隔 std::this_thread::sleep_for(std::chrono::seconds(1)); } } // 消费者线程函数 void consumer() { for (int i = 0; i < 5; ++i) { // P操作:申请消费权限(无数据则阻塞) sem_wait(&g_consumer_sem); // 消费数据 std::cout << "消费者:消费数据 " << g_data << std::endl; // V操作:通知生产者可以继续生产 sem_post(&g_producer_sem); // 模拟消费间隔 std::this_thread::sleep_for(std::chrono::seconds(1)); } } int main() { // 初始化信号量 sem_init(&g_producer_sem, 0, 1); sem_init(&g_consumer_sem, 0, 0); // 创建生产者和消费者线程 std::thread t_producer(producer); std::thread t_consumer(consumer); // 等待线程执行完毕 t_producer.join(); t_consumer.join(); // 销毁信号量 sem_destroy(&g_producer_sem); sem_destroy(&g_consumer_sem); return 0; } 编译与运行(GCC) bash 运行 # 无需C++20标准,链接线程库即可 g++ semaphore_linux.cpp -o semaphore_linux -pthread ./semaphore_linux 运行结果说明 生产者和消费者线程有序执行,生产者生产一个数据后,必须等待消费者消费完毕才能继续生产,消费者也只能在生产者生产后才能消费,实现了完美的线程协同。 五、信号量的常见应用场景 临界区资源互斥访问:使用二进制信号量替代互斥锁(std::mutex),实现共享资源(如全局变量、文件句柄)的原子操作。 资源限流:使用计数信号量控制并发访问数(如线程池最大并发数、接口限流、连接池最大连接数)。 线程间协同:实现 “生产者 - 消费者”“读者 - 写者” 等模型,解决线程间的等待 / 通知问题。 进程间通信:使用 Linux 命名信号量或 Windows 系统信号量,实现不同进程间的同步与互斥。 六、注意事项 避免死锁:申请信号量后,必须保证在所有分支(包括异常分支)中释放信号量,否则会导致其他线程永久阻塞。 C++20 兼容性:std::semaphore仅在 C++20 及以上标准支持,使用前需确认编译器版本(GCC 10+、Clang 11+、MSVC 2019+ 支持 C++20)。 信号量与互斥锁的区别: 互斥锁(std::mutex)只能由持有锁的线程释放,信号量可以由任意线程释放。 信号量支持计数限流,互斥锁仅支持单线程互斥访问。 互斥锁的性能通常优于二进制信号量,简单互斥场景优先使用互斥锁。 Linux 信号量初始化:无名信号量用于线程间通信时,pshared参数必须设为0。 总结 信号量是由 “计数器 + 等待 / 唤醒机制” 组成的同步工具,核心操作是 P(acquire/wait)和 V(release/post),且这两个操作均为原子操作。 信号量分为二进制信号量(互斥,0/1)和计数信号量(限流,非负整数),C++20 提供了标准实现,Linux 提供了<semaphore.h>兼容旧版本。 信号量可用于解决资源互斥、限流、线程协同等问题,使用时需避免死锁,简单互斥场景优先选择互斥锁以获得更好性能。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/6 20:38:21

大数据领域:数据清洗推动企业数字化转型

大数据领域&#xff1a;数据清洗推动企业数字化转型关键词&#xff1a;数据清洗、数据质量、企业数字化转型、大数据处理、数据治理、数据价值、数据生命周期摘要&#xff1a;在企业数字化转型的浪潮中&#xff0c;“数据"被称为新时代的"石油”。但未经处理的原始数…

作者头像 李华
网站建设 2026/5/6 14:05:40

Hadoop 助力大数据领域的精准营销

Hadoop 助力大数据领域的精准营销 关键词&#xff1a;Hadoop、大数据、精准营销、分布式计算、用户画像、数据挖掘、商业智能 摘要&#xff1a;在“酒香也怕巷子深”的数字时代&#xff0c;企业如何从海量数据中精准找到目标用户&#xff1f;Hadoop作为大数据领域的“基建狂魔”…

作者头像 李华
网站建设 2026/5/6 20:36:47

高可用的三件事——无状态化、水平扩展与故障转移的协同设计

高可用不是简单的冗余堆砌&#xff0c;而是无状态化、水平扩展与故障转移三者协同的艺术品 在掌握了系统压测方法论&#xff0c;能够准确评估系统容量边界后&#xff0c;我们面临一个更根本的挑战&#xff1a;如何让系统在真实流量冲击和故障发生时保持稳定&#xff1f;高可用架…

作者头像 李华
网站建设 2026/5/6 11:04:31

如何强制ANY CPU的.net程序按32位或64位模式运行?

强制指定运行位数的方法&#xff0c;&#xff0c;有没有执行某个exe让他按指定模式加载的方式&#xff1f;比如regasm.exe就有32位、64位模式已完成思考你想知道是否能像regasm.exe区分 32/64 位模式那样&#xff0c;在执行 EXE 时直接指定其运行位数&#xff08;而非编译 / 修…

作者头像 李华
网站建设 2026/5/6 20:34:34

被低估的Excel艺术大师:用REPT函数一键生成条形图与星级评分

谁说Excel图表只能通过插入菜单完成&#xff1f;一段简单的字符重复&#xff0c;就能创造出直观又灵活的数据可视化效果。 在日常数据分析中&#xff0c;我们经常需要快速对比数值大小或直观展示评分等级。传统方法可能是插入正式图表或编写复杂的条件格式&#xff0c;但有一种…

作者头像 李华