std::counting_semaphore计数器为0时调用acquire()的问题及编译器推荐
std::counting_semaphore计数器为0时调用acquire()的行为分析
问题核心
当std::counting_semaphore的计数器值被减至0后,若有其他线程调用acquire(),会触发什么行为?结合下方示例代码,线程t1将计数器降至0后,线程t3调用acquire()导致程序陷入永久阻塞,表现为“处理超时”,以下是具体分析。
示例代码
#include <iostream> #include <semaphore> #include <thread> #include <vector> std::vector<int> myVec{}; std::counting_semaphore<2> signal(1); void addElement() { std::cout << "In addElement() \n"; myVec.insert(myVec.end(), {0, 1, 0, 3}); std::cout << "Sender: Element added ." << '\n'; std::cout << "In addElement() value1 :" << signal.max() << "\n"; signal.release(); } void accessElement() { std::cout << "Waiter:: Waiting for data accessing." << '\n'; std::cout << "In accessElement() value1 :" << signal.max() << "\n"; signal.acquire(); std::cout << "updated the element........." << '\n'; } int main() { std::thread t1(accessElement); t1.join(); std::thread t3(accessElement); t3.join(); std::thread t2(addElement); t2.join(); }
行为分析
信号量初始化:示例中
signal是最大许可数为2的计数信号量,初始计数器值为1。线程执行顺序与阻塞原因:
- 主线程先启动
t1并调用join(),t1执行accessElement()时调用signal.acquire(),计数器从1减至0,t1完成后退出。 - 接着主线程启动
t3并调用join(),t3执行accessElement()时再次调用signal.acquire()——此时计数器已经是0,根据std::counting_semaphore的规则,acquire()会阻塞当前线程,直到有其他线程调用release()使计数器大于0。 - 但主线程要等
t3执行完成才会启动t2,而t2是唯一会调用signal.release()的线程,这就形成了死锁:t3阻塞等待计数器增加,t2永远没机会启动来释放信号量,最终程序陷入永久阻塞,表现为“处理超时”。
- 主线程先启动
acquire()的标准行为:
- 当计数器值>0时,
acquire()会原子性地将计数器减1,线程立即继续执行; - 当计数器值=0时,调用
acquire()的线程会被挂起,进入阻塞状态,直到有其他线程调用release()(每次release()会将计数器加1,最大不超过信号量的许可上限),线程才会被唤醒并执行计数器减1操作。
- 当计数器值>0时,
推荐在线编译器
可用于测试此类多线程示例的在线工具包括:
- 编译器资源管理器(Compiler Explorer):支持C++20及以上标准,能查看编译后的汇编代码,同时支持运行多线程程序观察行为
- 在线GDB调试器(Online GDB):支持设置断点、单步调试,便于追踪线程阻塞的具体位置
- CPP.sh:轻量便捷,可快速编译运行C++代码,适合快速验证多线程逻辑
内容的提问来源于stack exchange,提问作者Mangy
相关产品推荐
相关产品推荐

