广告

C++20 std::counting_semaphore 是什么?如何用它控制并发访问资源数量(实战指南)

C++20 std::counting_semaphore 是什么?

定义与核心概念

在并发编程中,std::counting_semaphore 是一种用于控制资源并发访问数量的同步原语。它属于 C++20 引入的标准同步库,用于管理一个数值计数,表示“可用资源的数量”。

与互斥锁(mutex)不同,计数信号量允许同时有多于一个线程进入临界区。你可以把它理解为一个带有容量上限的门禁系统:每次 acquire 会让一个“门票”离开,只有当门票数量大于 0 时才允许进入;release 会把一张门票放回去,允许更多线程进入。

C++20 std::counting_semaphore 是什么?如何用它控制并发访问资源数量(实战指南)

模板参数 N 定义了最大并发度,即信号量的容量。最大计数不可超过 N,确保资源池的上限在编译期就确定。实际容量在运行时由构造时传入的参数决定,但上限是模板参数的值。

#include <semaphore>
#include <thread>
#include <vector>
#include <iostream>std::counting_semaphore<5> sem(5); // 最大并发5个线程进入void work(int id) {sem.acquire(); // 尝试进入资源区域,若无可用,则阻塞std::cout << "Thread " << id << " entering" << std::endl;// 使用受保护的资源std::this_thread::sleep_for(std::chrono::milliseconds(100));std::cout << "Thread " << id << " leaving" << std::endl;sem.release(); // 释放一个资源单位
}int main() {std::vector<std::thread> ts;for (int i=0; i<10; ++i) ts.emplace_back(work, i);for (auto &t : ts) t.join();
}

核心接口与使用模式

基本操作:acquire、release、try_acquire

最常用的操作是 acquirerelease,它们分别代表获取一个资源并释放一个资源。调用 acquire 时如果资源不足,当前线程将被阻塞,直到有可用资源。

当需要非阻塞或带超时的获取时,可以使用 try_acquiretry_acquire_fortry_acquire_until。这让你可以在并发控制之外进行更灵活的策略决策。

#include <semaphore>
#include <thread>
#include <iostream>
#include <vector>
#include <chrono>std::counting_semaphore<3> s(3);void t(int id) {s.acquire();std::cout << "thread " << id << " acquired" << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));s.release();
}

要点:acquire/ release 是最基本的进入与退出资源区域的操作,确保在任意路径下资源的计数始终保持正确;try_acquire 系列函数提供非阻塞获取能力,便于实现降级策略或超时处理。

示例:实现资源保护的简单框架

下面的样例展示了如何将信号量用于保护一个有限资源池,并且避免手动记得 release。

在多线程环境中,允许的并发进入数量由并发容量决定,超出部分等待或放弃,取决于实现和策略。

#include <semaphore>
#include <thread>
#include <iostream>const int MAX_CONNS = 4;
std::counting_semaphore<MAX_CONNS> pool_sem(MAX_CONNS);void use_resource(int id) {pool_sem.acquire();std::cout << "Thread " << id << " acquired resource" << std::endl;// 模拟资源使用std::this_thread::sleep_for(std::chrono::milliseconds(120));std::cout << "Thread " << id << " releasing resource" << std::endl;pool_sem.release();
}

实战场景:限制并发访问资源数量(数据库连接、I/O、硬件设备)

示例:数据库连接池的并发控制

在高并发应用中,数据库连接是宝贵资源。使用 std::counting_semaphore 可以很容易地实现一个简单的连接池限流。

你只需要为连接数设定容量,线程需要连接前先调用 acquire,完成后再调用 release,即可确保同一时刻最多有设定数量的连接被占用。

#include <semaphore>
#include <thread>
#include <vector>
#include <iostream>
#include <chrono>const int MAX_CONN = 8;
std::counting_semaphore<MAX_CONN> conn_sem(MAX_CONN);void simulate_db_operation(int id) {conn_sem.acquire();std::cout << "Thread " << id << " acquired a DB connection" << std::endl;// 模拟数据库操作std::this_thread::sleep_for(std::chrono::milliseconds(150));conn_sem.release();std::cout << "Thread " << id << " released the DB connection" << std::endl;
}

示例:限流 I/O 或硬件设备访问

除了数据库,计数信号量也适用于限制对文件、网卡、其他外部设备的并发访问。

#include <semaphore>
#include <thread>
#include <chrono>
#include <iostream>std::counting_semaphore<4> io_sem(4);void drive_io(int i) {if (io_sem.try_acquire_for(std::chrono::milliseconds(50))) {std::cout << "IO operation by " << i << std::endl;// 模拟 I/O 操作std::this_thread::sleep_for(std::chrono::milliseconds(100));io_sem.release();} else {std::cout << "IO operation by " << i << " timed out" << std::endl;}
}

进阶用法:带超时等待、非阻塞获取与异常处理

超时等待与非阻塞获取

通过 try_acquire_fortry_acquire_until,你可以在规定时间内尝试获取资源,如果等待超时则执行备用策略。

要点:在并发场景中,错误处理和超时策略同样重要,确保能够在资源短缺时优雅地降级或重试。

#include <semaphore>
#include <thread>
#include <chrono>std::counting_semaphore<3> s(3);void try_with_timeout(int id) {if (s.try_acquire_for(std::chrono::milliseconds(200))) {// 使用资源std::this_thread::sleep_for(std::chrono::milliseconds(50));s.release();} else {// 超时处理// 重试或记录std::cout << "thread " << id << " timed out" << std::endl;}
}

广告

后端开发标签