C++ 多线程与线程同步面试题大全
一、基础概念
Q1: 进程和线程的区别?
| 维度 | 进程 | 线程 |
|---|---|---|
| 定义 | 资源分配的基本单位 | CPU 调度的基本单位 |
| 内存 | 独立地址空间 | 共享进程的地址空间 |
| 开销 | 创建/切换开销大 | 创建/切换开销小 |
| 通信 | IPC(管道、消息队列等) | 直接共享内存 |
| 安全性 | 进程间隔离,更安全 | 一个线程崩溃可能影响整个进程 |
Q2: 什么是线程安全?
答:多个线程同时访问共享资源时,程序行为仍然正确,不会出现数据竞争或不一致。
实现方式:
- 互斥锁(mutex)
- 原子操作(atomic)
- 线程局部存储(thread_local)
- 不可变数据
Q3: 什么是数据竞争(Data Race)?
答:两个或多个线程同时访问同一内存位置,且至少一个是写操作,且没有同步机制。
cpp
// 数据竞争示例
int counter = 0;
void increment() {
for (int i = 0; i < 10000; i++) {
counter++; // 非原子操作,存在竞争
}
}
int main() {
thread t1(increment);
thread t2(increment);
t1.join(); t2.join();
cout << counter; // 结果不确定,可能 < 20000
}
二、C++ 线程库
Q4: std::thread 的基本用法?
cpp
#include <thread>
void func(int x) { cout << x; }
int main() {
// 方式1: 函数指针
thread t1(func, 42);
// 方式2: Lambda
thread t2([](){ cout << "lambda"; });
// 方式3: 函数对象
struct Functor {
void operator()() { cout << "functor"; }
};
thread t3(Functor{});
t1.join(); // 等待完成
t2.join();
t3.detach(); // 分离,后台运行
}
Q5: join() 和 detach() 的区别?
| 方法 | 行为 | 使用场景 |
|---|---|---|
join() |
阻塞当前线程,等待目标线程完成 | 需要线程结果或确保完成 |
detach() |
分离线程,后台独立运行 | 不关心线程何时结束 |
注意 :线程对象销毁前必须调用其中之一,否则程序终止(调用 std::terminate)。
Q6: 如何向线程传递参数?
cpp
void func(int a, string& s) { s = "modified"; }
int main() {
string str = "original";
// 错误:默认是拷贝传参
thread t1(func, 1, str); // str 不会被修改
// 正确:使用 std::ref
thread t2(func, 1, ref(str)); // str 会被修改
t1.join();
t2.join();
}
std::ref() 的作用
ref(str2) 创建一个 引用包装器 (std::reference_wrapper),让 std::thread 传递真正的引用而不是拷贝。
为什么需要 ref()?
cpp
void func_ref(int a, string& s); // 需要引用
// 不用 ref:
thread t(func_ref, 1, str2); // ❌ thread 会拷贝 str2
// 拷贝的临时对象无法绑定到 string&
// 用 ref:
thread t(func_ref, 1, ref(str2)); // ✓ 传递 str2 的引用包装器
// 线程内部会解包成真正的引用
原理图示
不用 ref():
str2 ──拷贝──► thread 内部存储 ──► func_ref(临时对象) ❌ 编译失败
用 ref():
str2 ──ref()包装──► reference_wrapper ──► func_ref(str2的引用) ✓
简单理解
| 写法 | 效果 |
|---|---|
thread t(func, str) |
传递 str 的拷贝 |
thread t(func, ref(str)) |
传递 str 的引用 |
thread t(func, cref(str)) |
传递 str 的 const 引用 |
头文件
cpp
#include <functional> // std::ref, std::cref
// 注意:<thread> 通常已包含,不需单独引入
总结 :ref() 告诉 std::thread:"不要拷贝这个变量,我要传引用!"
三、互斥锁
Q7: std::mutex 的基本用法?
cpp
#include <mutex>
mutex mtx;
int counter = 0;
void safe_increment() {
mtx.lock();
counter++;
mtx.unlock();
}
// 更好的方式:RAII
void better_increment() {
lock_guard<mutex> lock(mtx); // 构造时加锁,析构时解锁
counter++;
} // 自动解锁
Q8: lock_guard 和 unique_lock 的区别?
| 特性 | lock_guard | unique_lock |
|---|---|---|
| 灵活性 | 低(只能构造时锁定) | 高(可延迟锁定、手动解锁) |
| 开销 | 小 | 稍大 |
| 移动 | 不可移动 | 可移动 |
| 配合条件变量 | 不可以 | 可以 |
cpp
// lock_guard: 简单场景
{
lock_guard<mutex> lock(mtx);
// 临界区
}
// unique_lock: 需要灵活控制
{
unique_lock<mutex> lock(mtx, defer_lock); // 延迟锁定
// ... 其他操作
lock.lock(); // 手动锁定
// 临界区
lock.unlock(); // 可手动解锁
}
Q9: 什么是死锁?如何避免?
死锁条件(四个同时满足):
- 互斥
- 持有并等待
- 不可剥夺
- 循环等待
避免方法:
cpp
// 方法1: 固定加锁顺序
mutex m1, m2;
// 所有线程都先锁 m1 再锁 m2
// 方法2: std::lock 同时锁多个
{
lock(m1, m2); // 原子地锁定两个
lock_guard<mutex> lg1(m1, adopt_lock);
lock_guard<mutex> lg2(m2, adopt_lock);
}
// 方法3: C++17 scoped_lock
{
scoped_lock lock(m1, m2); // 更简洁
}
// 方法4: try_lock 非阻塞
if (mtx.try_lock()) {
// 成功获得锁
mtx.unlock();
}
Q10: 各种 mutex 类型的区别?
| 类型 | 特点 |
|---|---|
mutex |
基本互斥锁 |
recursive_mutex |
允许同一线程多次锁定 |
timed_mutex |
支持超时锁定 |
shared_mutex (C++17) |
读写锁(多读单写) |
cpp
// recursive_mutex 示例
recursive_mutex rmtx;
void recursive_func(int n) {
rmtx.lock();
if (n > 0) recursive_func(n - 1); // 不会死锁
rmtx.unlock();
}
// shared_mutex 读写锁
shared_mutex rw_mtx;
void reader() {
shared_lock lock(rw_mtx); // 共享锁,多个读者可同时持有
// 读操作
}
void writer() {
unique_lock lock(rw_mtx); // 独占锁
// 写操作
}
四、条件变量
Q11: 条件变量的作用和用法?
作用:线程间同步,一个线程等待某个条件,另一个线程通知条件满足。
cpp
#include <condition_variable>
mutex mtx;
condition_variable cv;
bool ready = false;
void worker() {
unique_lock<mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // 等待条件
cout << "Worker 开始工作" << endl;
}
void trigger() {
{
lock_guard<mutex> lock(mtx);
ready = true;
}
cv.notify_one(); // 通知一个等待线程
}
Q12: 为什么 wait() 需要配合 while 循环或谓词?
答 :防止虚假唤醒(Spurious Wakeup)。
cpp
// 错误:可能虚假唤醒
cv.wait(lock);
// 条件可能不满足就被唤醒
// 正确:使用谓词
cv.wait(lock, []{ return condition; });
// 等价于
while (!condition) {
cv.wait(lock);
}
Q13: notify_one() 和 notify_all() 的区别?
| 方法 | 行为 |
|---|---|
notify_one() |
唤醒一个等待线程 |
notify_all() |
唤醒所有等待线程 |
使用建议:
- 只有一个线程能处理 →
notify_one() - 多个线程都可能需要响应 →
notify_all()
五、原子操作
Q14: std::atomic 的作用?
答:提供无锁的原子操作,避免数据竞争。
cpp
#include <atomic>
atomic<int> counter{0};
void increment() {
for (int i = 0; i < 10000; i++) {
counter++; // 原子操作,线程安全
// 或 counter.fetch_add(1);
}
}
Q15: 原子操作的内存序(Memory Order)?
| 内存序 | 说明 |
|---|---|
memory_order_relaxed |
只保证原子性,不保证顺序 |
memory_order_acquire |
读操作,后续读写不能重排到前面 |
memory_order_release |
写操作,之前读写不能重排到后面 |
memory_order_seq_cst |
顺序一致(默认,最严格) |
cpp
atomic<bool> flag{false};
int data = 0;
void producer() {
data = 42;
flag.store(true, memory_order_release);
}
void consumer() {
while (!flag.load(memory_order_acquire));
cout << data; // 保证看到 42
}
六、异步任务
Q16: std::async 和 std::thread 的区别?
| 特性 | std::async | std::thread |
|---|---|---|
| 返回值 | 通过 future 获取 | 无直接支持 |
| 异常 | 可通过 future 捕获 | 需要手动处理 |
| 执行策略 | 可选同步/异步 | 总是新线程 |
cpp
#include <future>
int compute() { return 42; }
int main() {
// async 方式
future<int> fut = async(launch::async, compute);
int result = fut.get(); // 获取返回值
// thread 方式需要额外处理
int result2;
thread t([&result2]{ result2 = compute(); });
t.join();
}
Q17: std::future 和 std::promise 的关系?
cpp
promise<int> prom;
future<int> fut = prom.get_future();
// 生产者线程
thread producer([&prom]{
int result = 42;
prom.set_value(result); // 设置值
});
// 消费者线程
thread consumer([&fut]{
int value = fut.get(); // 阻塞等待值
cout << value;
});
producer.join();
consumer.join();
七、线程池
Q18: 为什么需要线程池?
答:
- 减少开销:避免频繁创建/销毁线程
- 资源控制:限制并发线程数
- 任务队列:管理待执行任务
Q19: 简单线程池实现?
cpp
class ThreadPool {
vector<thread> workers;
queue<function<void()>> tasks;
mutex mtx;
condition_variable cv;
bool stop = false;
public:
ThreadPool(size_t n) {
for (size_t i = 0; i < n; i++) {
workers.emplace_back([this] {
while (true) {
function<void()> task;
{
unique_lock<mutex> lock(mtx);
cv.wait(lock, [this]{
return stop || !tasks.empty();
});
if (stop && tasks.empty()) return;
task = move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
template<class F>
void enqueue(F&& f) {
{
lock_guard<mutex> lock(mtx);
tasks.emplace(forward<F>(f));
}
cv.notify_one();
}
~ThreadPool() {
{
lock_guard<mutex> lock(mtx);
stop = true;
}
cv.notify_all();
for (auto& w : workers) w.join();
}
};
八、经典问题
Q20: 生产者-消费者问题
cpp
template<typename T>
class BlockingQueue {
queue<T> q;
mutex mtx;
condition_variable not_empty, not_full;
size_t capacity;
public:
BlockingQueue(size_t cap) : capacity(cap) {}
void push(T item) {
unique_lock<mutex> lock(mtx);
not_full.wait(lock, [this]{ return q.size() < capacity; });
q.push(move(item));
not_empty.notify_one();
}
T pop() {
unique_lock<mutex> lock(mtx);
not_empty.wait(lock, [this]{ return !q.empty(); });
T item = move(q.front());
q.pop();
not_full.notify_one();
return item;
}
};
Q21: 读写锁使用场景?
答:读多写少的场景,如缓存、配置管理。
cpp
shared_mutex rw_mtx;
map<string, string> cache;
string read(const string& key) {
shared_lock lock(rw_mtx); // 多个读者可同时访问
return cache[key];
}
void write(const string& key, const string& val) {
unique_lock lock(rw_mtx); // 写者独占
cache[key] = val;
}
九、快速记忆表
| 问题 | 解决方案 |
|---|---|
| 数据竞争 | mutex / atomic |
| 死锁 | scoped_lock / 固定顺序 |
| 条件等待 | condition_variable |
| 获取返回值 | async + future |
| 读多写少 | shared_mutex |
| 频繁创建线程 | 线程池 |