面试题答案
一键面试可能出现的线程安全问题
- 内存可见性:不同线程可能缓存共享资源的不同副本。当一个线程修改了共享资源,其他线程可能不会立即看到修改后的值,导致程序逻辑错误。例如,一个线程更新了共享资源的某个字段,另一个线程获取该共享资源的常量引用后,却仍然使用旧的值。
- 竞态条件:多个线程同时访问和修改共享资源,其最终结果依赖于线程执行的相对顺序。即使函数返回常量引用,共享资源本身可能在其他地方被修改。比如,在获取常量引用之前,共享资源可能正处于部分修改的状态,这就导致获取到的引用指向一个不一致的状态。
解决方案
- 互斥锁(Mutex)
- 实现方式:在访问共享资源前加锁,访问完成后解锁。在返回共享资源的常量引用的函数中,使用互斥锁保护共享资源的访问。
- 优点:简单直接,适用于大多数场景,能有效避免竞态条件和保证内存可见性。
- 缺点:加锁和解锁操作会带来性能开销,尤其是在高并发情况下,可能导致线程频繁等待锁,降低系统整体性能。
- 适用场景:适用于对性能要求不是极高,共享资源访问频率不是特别频繁的场景。
示例代码:
#include <iostream>
#include <mutex>
std::mutex mtx;
class SharedResource {
public:
int data;
SharedResource() : data(0) {}
};
SharedResource shared;
const SharedResource& getSharedResource() {
std::lock_guard<std::mutex> lock(mtx);
return shared;
}
- 读写锁(Read - Write Lock)
- 实现方式:允许多个线程同时进行读操作(获取常量引用属于读操作),但只允许一个线程进行写操作。写操作时,其他读和写操作都要等待。
- 优点:读操作并发性能好,适用于读多写少的场景,能有效提高系统性能。因为读操作不修改共享资源,多个线程同时读不会产生竞态条件,所以可以并行执行。
- 缺点:实现相对复杂,写操作的同步开销较大,因为每次写操作都需要独占锁,可能导致读线程等待。
- 适用场景:适用于读操作远远多于写操作的场景,例如配置文件读取等场景,配置文件更新频率低,但读取频率高。
示例代码(以C++17的std::shared_mutex为例):
#include <iostream>
#include <shared_mutex>
std::shared_mutex rw_mtx;
class SharedResource {
public:
int data;
SharedResource() : data(0) {}
};
SharedResource shared;
const SharedResource& getSharedResource() {
std::shared_lock<std::shared_mutex> lock(rw_mtx);
return shared;
}
- 线程本地存储(Thread - Local Storage,TLS)
- 实现方式:为每个线程提供一份独立的共享资源副本,这样每个线程对共享资源的访问都是线程本地的,不存在线程间的竞争。
- 优点:彻底避免了线程间的竞争,性能非常好,因为不需要锁操作。
- 缺点:如果共享资源需要在多个线程间保持一致性,TLS就不适用了。而且,会增加内存消耗,因为每个线程都有一份副本。
- 适用场景:适用于共享资源不需要在多个线程间保持一致,每个线程独立使用的场景,比如日志记录,每个线程记录自己的日志,不需要与其他线程同步。
示例代码(以C++的__thread关键字为例,不同编译器可能有不同实现):
#include <iostream>
__thread int localData;
void someFunction() {
localData = 42;
std::cout << "Thread - local data: " << localData << std::endl;
}