面试题答案
一键面试可能遇到的线程安全问题
- 数据竞争:多个线程同时对容器进行遍历和修改操作时,可能会导致数据不一致。例如,一个线程在遍历
std::map
时,另一个线程删除了正在遍历的元素,会导致未定义行为。 - 迭代器失效:当一个线程在遍历容器过程中,另一个线程对容器进行插入或删除操作,可能使迭代器失效,导致程序崩溃或出现未定义行为。
解决方案及优缺点分析
- 互斥锁(Mutex)
- 实现:在遍历容器前加锁,遍历结束后解锁。例如:
std::mutex mtx;
std::map<int, int> myMap;
void traverseMap() {
std::lock_guard<std::mutex> lock(mtx);
for (auto it = myMap.begin(); it != myMap.end(); ++it) {
// 遍历操作
}
}
- **优点**
- **性能**:实现简单,对于简单场景性能开销较小。
- **资源占用**:资源占用少,只需要一个互斥锁。
- **代码复杂度**:代码复杂度低,容易理解和实现。
- **缺点**
- **性能**:如果频繁加锁解锁,在高并发场景下会成为性能瓶颈。
- **资源占用**:虽然互斥锁本身占用资源少,但可能导致线程等待,增加系统资源消耗。
- **代码复杂度**:在复杂的多线程场景中,锁的管理可能变得复杂,容易出现死锁。
2. 读写锁(Read - Write Lock) - 实现:读操作时多个线程可以同时进行,写操作时需要独占锁。例如:
std::shared_mutex rwMutex;
std::unordered_map<int, int> myUnorderedMap;
void traverseUnorderedMap() {
std::shared_lock<std::shared_mutex> lock(rwMutex);
for (auto it = myUnorderedMap.begin(); it != myUnorderedMap.end(); ++it) {
// 遍历操作
}
}
- **优点**
- **性能**:读操作并发性能好,适合读多写少的场景,能提高整体性能。
- **资源占用**:相比互斥锁,在高并发读场景下,线程等待时间减少,资源利用率更高。
- **代码复杂度**:相对简单,读写锁的语义清晰,实现难度适中。
- **缺点**
- **性能**:写操作时仍然独占锁,如果写操作频繁,会影响整体性能。
- **资源占用**:读写锁的实现比互斥锁复杂,占用一定的系统资源。
- **代码复杂度**:相比简单互斥锁,在处理读写锁的逻辑上稍复杂,需要注意读写操作的合理控制。
3. 使用线程安全的容器
- 实现:如 boost::container::static_vector
等线程安全容器,其内部已经实现了线程安全机制。使用方式与普通容器类似:
boost::container::static_vector<int, 10> myVector;
for (auto it = myVector.begin(); it != myVector.end(); ++it) {
// 遍历操作
}
- **优点**
- **性能**:在保证线程安全的前提下,通常经过优化,性能较好。
- **资源占用**:由容器内部管理线程安全机制,合理利用资源,减少用户管理成本。
- **代码复杂度**:使用方式与普通容器类似,代码复杂度低,易于使用。
- **缺点**
- **性能**:不同实现的线程安全容器性能差异较大,可能在某些场景下不如手动加锁优化。
- **资源占用**:相比普通容器,可能会占用更多的内存空间用于线程安全机制。
- **代码复杂度**:依赖特定库,可能增加项目的依赖和部署复杂度。