如何在C++中实现多线程访问资源池双资源的同步管理
多线程资源同步问题分析与优化方案
问题背景
模拟6个线程访问共享资源池的场景:资源池包含6个初始可用的资源,每个线程需要同时占用两个资源,使用完成后释放。要求避免竞态条件,同时保证不共享资源的线程可并发执行(如T1和T3)、共享资源的线程互斥执行(如T1和T2),拒绝全局锁导致的效率低下问题。
线程与资源的对应关系:
- T1(coupe):res[0]、res[1]
- T2(epee):res[1]、res[2]
- T3(chandelier):res[2]、res[3]
- T4(bague):res[3]、res[4]
- T5(table):res[4]、res[5]
- T6(porte):res[0]、res[5]
现有方案的错误分析
你提出的「每个资源对应一个信号量」的思路方向正确,但代码存在以下关键问题:
- 信号量初始化错误:
std::binary_semaphore sem0{0}将初始值设为0,意味着资源一开始不可用,但实际资源初始状态为可用,正确初始值应为1。 - 竞态条件未解决:检查
el1数组状态与获取信号量的操作不是原子的。例如线程A检查el1[0]为1后,线程B可能抢先占用该资源并修改el1[0]为0,此时线程A再执行el1[0] = 0会导致状态错误。 - 信号量逻辑错误:仅当
el1显示资源被占用时才调用acquire,但初始信号量为0,第一次执行会直接阻塞线程,无法正常获取资源。
修正后的单资源信号量实现方案
以下是正确的实现,核心思路是用二元信号量直接管理每个资源的状态,去掉冗余的bool数组,确保资源的获取和释放是原子操作:
#include <iostream> #include <string> #include <thread> #include <semaphore> #include <chrono> #include <syncstream> #include <algorithm> // 用于std::min/std::max const int64_t tempsCreationObjetsMS = 3000; // 每个资源对应一个二元信号量,初始值1表示资源可用 std::binary_semaphore sem_res[6] = { std::binary_semaphore{1}, std::binary_semaphore{1}, std::binary_semaphore{1}, std::binary_semaphore{1}, std::binary_semaphore{1}, std::binary_semaphore{1} }; void creer_thread(const std::string& nom, int res_a, int res_b) { std::osyncstream(std::cout) << "Je suis le thread " << nom << std::endl; // 按资源编号从小到大获取,避免死锁 int first_res = std::min(res_a, res_b); int second_res = std::max(res_a, res_b); // 获取第一个资源 sem_res[first_res].acquire(); std::osyncstream(std::cout) << nom << " a acquis la resource " << first_res << std::endl; // 获取第二个资源 sem_res[second_res].acquire(); std::osyncstream(std::cout) << nom << " a acquis la resource " << second_res << std::endl; // 使用资源 std::osyncstream(std::cout) << nom << " en creation." << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(tempsCreationObjetsMS)); std::osyncstream(std::cout) << nom << " cree." << std::endl; // 释放资源(顺序不影响) sem_res[second_res].release(); sem_res[first_res].release(); std::osyncstream(std::cout) << nom << " a libere les ressources " << first_res << " et " << second_res << std::endl; } int main(void) { std::thread th1(creer_thread, "coupe", 0, 1); std::thread th2(creer_thread, "epee", 1, 2); std::thread th3(creer_thread, "chandelier", 2, 3); std::thread th4(creer_thread, "bague", 3, 4); std::thread th5(creer_thread, "table", 4, 5); std::thread th6(creer_thread, "porte", 0, 5); th1.join(); th2.join(); th3.join(); th4.join(); th5.join(); th6.join(); return 0; }
方案优势
- 无竞态条件:信号量的
acquire和release是原子操作,确保资源不会被多个线程同时占用。 - 高效并发:不共享资源的线程(如T1和T3)可以同时执行,不会被全局锁阻塞。
- 避免死锁:通过按资源编号从小到大的顺序获取资源,破坏了死锁的"环形等待"条件,彻底避免死锁风险。
- 代码简洁:去掉了冗余的
bool数组,直接用信号量管理资源状态,逻辑更清晰。
替代方案:使用std::mutex实现
如果更倾向于用互斥锁,也可以为每个资源分配一个std::mutex,实现逻辑与信号量方案一致:
#include <iostream> #include <string> #include <thread> #include <mutex> #include <chrono> #include <syncstream> #include <algorithm> const int64_t tempsCreationObjetsMS = 3000; std::mutex res_mutex[6]; void creer_thread(const std::string& nom, int res_a, int res_b) { std::osyncstream(std::cout) << "Je suis le thread " << nom << std::endl; int first_res = std::min(res_a, res_b); int second_res = std::max(res_a, res_b); // 使用lock_guard实现RAII,避免异常导致锁无法释放 std::lock_guard<std::mutex> lock1(res_mutex[first_res]); std::osyncstream(std::cout) << nom << " a bloque la resource " << first_res << std::endl; std::lock_guard<std::mutex> lock2(res_mutex[second_res]); std::osyncstream(std::cout) << nom << " a bloque la resource " << second_res << std::endl; std::osyncstream(std::cout) << nom << " en creation." << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(tempsCreationObjetsMS)); std::osyncstream(std::cout) << nom << " cree." << std::endl; // lock_guard会自动释放锁,无需手动unlock std::osyncstream(std::cout) << nom << " a debloque les ressources " << first_res << " et " << second_res << std::endl; } // main函数与信号量方案一致,此处省略
注意事项
使用std::mutex时,优先搭配std::lock_guard或std::unique_lock实现RAII,即使线程执行过程中抛出异常,锁也会在对象销毁时自动释放,保证资源状态正确。
内容的提问来源于stack exchange,提问作者Nicolas Cinq-Mars
相关产品推荐
相关产品推荐

