如何让两个线程安全访问C++类中的共享vector资源
嘿,我来帮你梳理下这个C++线程安全的问题。你现在的场景是两个线程同时遍历databases_list_这个vector,还会调用其中database对象的接口,要保障安全可以从这几个层面一步步来:
1. 先搞定is_work_的线程可见性问题
你现在用普通bool作为线程循环的终止条件,但普通bool不是原子类型,跨线程读写会有可见性隐患——比如主线程把is_work_设为false,线程#1可能迟迟看不到这个修改,导致线程无法正常退出。所以第一步要把它改成原子类型:
#include <atomic> class Worker { private: // ... 其他成员不变 std::atomic<bool> is_work_{ false }; // 替换原来的bool类型 };
这样不管哪个线程修改is_work_,其他线程都能立刻看到最新值。
2. 保障databases_list_的遍历安全
首先得明确一个前提:如果databases_list_的结构不会被修改(也就是不会调用push_back、pop_back、erase这类改变vector大小或元素位置的操作),那多个线程同时只读遍历这个vector是安全的——因为vector的迭代器在结构稳定时不会失效,纯读操作也不会产生数据竞争。
但如果你的业务里存在其他线程(比如主线程)会修改这个vector的结构(比如添加/删除database对象),那必须给vector加锁保护:
方案:用互斥锁包裹所有vector访问
给Worker类加一个std::mutex成员,然后在所有访问databases_list_的地方(包括遍历、修改)都用锁包裹:
#include <mutex> class Worker { private: // ... 其他成员不变 std::vector<std::unique_ptr<database>> databases_list_; std::mutex db_list_mutex_; // 专门保护databases_list_的互斥锁 };
然后修改两个线程函数:
void Worker::check_oo() { while (is_work_) { // std::lock_guard会自动在作用域开始加锁,结束时解锁,避免忘记解锁 std::lock_guard<std::mutex> lock(db_list_mutex_); for (auto& db : databases_list_) { auto db_cfg = db->get_cfg(); data_handler_.set_database(db_cfg.db_server); auto db_respond = db->send_command(receive_cmd, command_type::get_data); } std::this_thread::sleep_for(std::chrono::seconds(processing_period_1)); } } void Worker::check_db() { while (is_work_) { std::lock_guard<std::mutex> lock(db_list_mutex_); for (auto& db : databases_list_) { auto db_cfg = db->get_cfg(); data_handler_.set_database(db_cfg.db_server); auto db_respond = db->send_command(receive_cmd, command_type::get_stat); } std::this_thread::sleep_for(std::chrono::seconds(processing_period_2)); } }
要是有其他修改databases_list_的代码(比如添加新database的方法),也要用同样的std::lock_guard加锁,确保同一时间只有一个线程在操作这个vector。
3. 别漏了database对象本身的线程安全
上面的锁只保护了databases_list_这个容器的访问,但两个线程可能同时调用同一个database对象的get_cfg()或send_command()方法。如果database类的这些接口不是线程安全的(比如内部有共享的非原子变量,或者有写操作),那即使容器访问安全了,还是会出现数据竞争。
所以你需要检查database类的实现:
- 如果
get_cfg()是纯只读操作,返回的是值或者内部没有修改逻辑,那没问题; - 如果
send_command()会修改database对象的内部状态,那得给database类内部加锁(比如每个database实例自己带一个std::mutex),或者用其他同步机制保障线程安全。
4. 优化:减少锁的持有时间提升并发效率
如果遍历vector的时间很长,或者send_command()是耗时操作,上面的方案会导致两个线程互相等待,降低并发效率。这时候可以考虑先复制一份容器的快照,让锁只在复制阶段持有,后续遍历快照不需要锁:
void Worker::check_oo() { while (is_work_) { std::vector<database*> db_ptrs; { std::lock_guard<std::mutex> lock(db_list_mutex_); db_ptrs.reserve(databases_list_.size()); // 先把所有database的指针收集起来 for (auto& db : databases_list_) { db_ptrs.push_back(db.get()); } } // 这里锁已经释放了,后面的耗时操作不需要持有锁 for (auto db_ptr : db_ptrs) { auto db_cfg = db_ptr->get_cfg(); data_handler_.set_database(db_cfg.db_server); auto db_respond = db_ptr->send_command(receive_cmd, command_type::get_data); } std::this_thread::sleep_for(std::chrono::seconds(processing_period_1)); } }
这个方案的好处是锁的持有时间极短,两个线程的并发效率更高,但要注意:如果这时候有线程删除了某个database对象,db_ptr就会变成悬空指针。所以这种方式只适合databases_list_的元素不会被删除的场景,或者你可以把std::unique_ptr<database>改成std::shared_ptr<database>——这样即使容器里的元素被移除,只要还有线程持有shared_ptr,database对象就不会被销毁,避免悬空指针问题。
最后总结下关键步骤
- 把
is_work_替换成std::atomic<bool>,解决线程可见性问题; - 给
databases_list_添加互斥锁,保护所有容器访问操作; - 确保
database类的接口是线程安全的; - 根据业务场景优化锁的持有时间,提升并发效率。
内容的提问来源于stack exchange,提问作者pipetka

