面试题答案
一键面试多线程C++程序中异常处理面临的特殊挑战
- 数据竞争:不同线程对共享资源的访问可能因异常抛出而导致数据不一致。例如,一个线程在更新共享数据结构过程中抛出异常,其他线程可能访问到部分更新的数据。
- 死锁:当线程持有锁时抛出异常,如果没有正确处理,可能导致锁无法释放,进而造成死锁。比如,线程A持有锁L1,准备获取锁L2时抛出异常,而线程B持有锁L2等待获取锁L1,就会产生死锁。
- 线程状态安全:异常抛出可能导致线程处于未定义状态,影响其他线程依赖该线程的后续操作。例如,一个线程负责初始化共享资源,若初始化过程中抛出异常,其他线程可能使用到未初始化的资源。
应对挑战的设计模式和最佳实践
- 资源管理(RAII):使用RAII机制管理资源,如锁、文件句柄等。例如,
std::lock_guard
和std::unique_lock
会在对象析构时自动释放锁,避免因异常导致锁未释放。
#include <mutex>
std::mutex mtx;
void function() {
std::lock_guard<std::mutex> lock(mtx);
// 操作共享资源,异常时lock自动析构释放锁
}
- 线程本地存储(TLS):将每个线程的数据独立存储,避免共享数据带来的数据竞争和异常处理问题。
thread_local
关键字可用于声明线程本地变量。
thread_local int threadSpecificData;
void threadFunction() {
threadSpecificData = 42;
// 该线程对threadSpecificData的操作不会影响其他线程
}
- 异常安全的设计:编写函数时遵循异常安全原则,如提供“基本保证”(函数失败时不泄露资源且对象处于有效状态)和“强保证”(函数要么成功完成,要么没有任何影响)。例如,在修改共享数据结构前,先创建临时副本,修改完成后再交换。
class SharedData {
std::vector<int> data;
public:
void updateData(const std::vector<int>& newData) {
std::vector<int> temp = newData;
// 对temp进行操作
std::lock_guard<std::mutex> lock(mtx);
data.swap(temp);
}
};
- 异常传播和处理:在线程入口函数捕获异常,避免异常跨线程传播。例如:
void threadEntry() {
try {
// 线程具体逻辑
} catch(...) {
// 处理异常,记录日志等
}
}
- 使用事务机制:对于涉及多个共享资源修改的操作,可借鉴数据库事务概念,要么所有操作成功,要么都回滚。例如,可自定义一个事务类,管理锁和资源操作。
class Transaction {
std::vector<std::mutex*> locks;
std::vector<std::function<void()>> rollbacks;
public:
void lock(std::mutex& m) {
m.lock();
locks.push_back(&m);
}
void addRollback(std::function<void()> func) {
rollbacks.push_back(func);
}
~Transaction() {
if (rollbacks.size() > 0) {
for (auto it = rollbacks.rbegin(); it != rollbacks.rend(); ++it) {
(*it)();
}
}
for (auto m : locks) {
m->unlock();
}
}
void commit() {
rollbacks.clear();
for (auto m : locks) {
m->unlock();
}
}
};
使用示例:
std::mutex m1, m2;
void modifySharedResources() {
Transaction tx;
tx.lock(m1);
tx.lock(m2);
// 假设这两个函数修改共享资源且有回滚操作
tx.addRollback([]() { /* 回滚m1相关操作 */ });
tx.addRollback([]() { /* 回滚m2相关操作 */ });
try {
// 修改共享资源操作
tx.commit();
} catch(...) {
// 异常时事务析构自动回滚
}
}