多线程面试题与实战
定义与作用
多线程面试聚焦死锁排查、数据竞争检测、锁粒度优化和无锁编程概念。本章通过飞翔科技场景展示四个核心考点的实战解法。
核心原理
死锁四条件
数据竞争检测
完整示例
示例一:死锁案例与三种解法
场景说明:飞翔科技转账系统出现死锁,白歌用三种方案修复。
#include <iostream>
#include <mutex>
#include <thread>
#include <string>
#include <chrono>
class Account {
public:
explicit Account(std::string name, int balance)
: name_(std::move(name)), balance_(balance) {}
std::string name_;
int balance_;
std::mutex mtx_;
};
// ❌ 死锁代码:两线程以相反顺序获取锁
void transferDeadlock(Account& from, Account& to, int amount) {
std::lock_guard<std::mutex> lockFrom(from.mtx_);
std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 加剧死锁
std::lock_guard<std::mutex> lockTo(to.mtx_);
from.balance_ -= amount;
to.balance_ += amount;
}
// ✅ 方案一:std::lock 同时加多锁
void transferLock(Account& from, Account& to, int amount) {
std::lock(from.mtx_, to.mtx_); // 原子同时加锁,避免死锁
std::lock_guard<std::mutex> lockFrom(from.mtx_, std::adopt_lock);
std::lock_guard<std::mutex> lockTo (to.mtx_, std::adopt_lock);
from.balance_ -= amount;
to.balance_ += amount;
std::cout << " 转账: " << from.name_ << " → " << to.name_
<< " ¥" << amount << "\n";
}
// ✅ 方案二:scoped_lock (C++17)
void transferScoped(Account& from, Account& to, int amount) {
std::scoped_lock lock(from.mtx_, to.mtx_);
from.balance_ -= amount;
to.balance_ += amount;
}
// ✅ 方案三:固定锁顺序
void transferHierarchy(Account& from, Account& to, int amount) {
// 总是先锁地址小的 mutex
if (&from < &to) {
std::lock_guard<std::mutex> l1(from.mtx_);
std::lock_guard<std::mutex> l2(to.mtx_);
from.balance_ -= amount;
to.balance_ += amount;
} else {
std::lock_guard<std::mutex> l1(to.mtx_);
std::lock_guard<std::mutex> l2(from.mtx_);
from.balance_ -= amount;
to.balance_ += amount;
}
}
int main() {
std::cout << "=== 飞翔科技转账系统 ===\n\n";
Account a("大翔", 10000);
Account b("孔蓝", 5000);
// 方案一:std::lock
std::cout << "--- std::lock 方案 ---\n";
std::thread t1(transferLock, std::ref(a), std::ref(b), 1000);
std::thread t2(transferLock, std::ref(b), std::ref(a), 500);
t1.join(); t2.join();
std::cout << a.name_ << " 余额: ¥" << a.balance_ << "\n";
std::cout << b.name_ << " 余额: ¥" << b.balance_ << "\n";
}
预期输出:
=== 飞翔科技转账系统 ===
--- std::lock 方案 ---
转账: 大翔 → 孔蓝 ¥1000
转账: 孔蓝 → 大翔 ¥500
大翔 余额: ¥9500
孔蓝 余额: ¥5500
逐段分析:
std::lock(l1, l2, ...)用死锁避免算法同时获取多个锁std::adopt_lock表示 mutex 已锁定,lock_guard只负责解锁std::scoped_lock(C++17) 是lock_guard的变参版,推荐使用- 固定锁顺序要求所有线程按同一顺序加锁
示例二:生产者-消费者与锁粒度
场景说明:李眉实现飞翔科技的异步日志队列。
#include <iostream>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <vector>
#include <string>
#include <chrono>
class AsyncLogger {
public:
void produce(const std::string& msg) {
{
std::lock_guard<std::mutex> lock(mtx_);
queue_.push(msg);
} // 锁在此释放,缩小临界区
cv_.notify_one();
}
void consume() {
while (running_) {
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this] { return !queue_.empty() || !running_; });
while (!queue_.empty()) {
std::string msg = std::move(queue_.front());
queue_.pop();
lock.unlock(); // 处理时释放锁,缩小临界区
// 模拟写入磁盘(耗时操作在锁外)
std::this_thread::sleep_for(std::chrono::milliseconds(50));
std::cout << " [写入磁盘] " << msg << "\n";
lock.lock(); // 重新获取锁继续处理
}
}
}
void stop() {
{
std::lock_guard<std::mutex> lock(mtx_);
running_ = false;
}
cv_.notify_all();
}
private:
std::queue<std::string> queue_;
std::mutex mtx_;
std::condition_variable cv_;
bool running_ = true;
};
int main() {
std::cout << "=== 飞翔科技异步日志队列 ===\n\n";
AsyncLogger logger;
// 消费者线程
std::thread consumer(&AsyncLogger::consume, &logger);
// 模拟用户请求产生日志
std::thread producers[3];
std::string names[] = {"黄俪-前端", "小崔-后端", "白歌-架构"};
for (int i = 0; i < 3; ++i) {
producers[i] = std::thread([&logger, name = names[i]]() {
for (int j = 1; j <= 3; ++j) {
logger.produce(std::string(name) + ": 请求 #" + std::to_string(j));
std::this_thread::sleep_for(std::chrono::milliseconds(30));
}
});
}
for (auto& t : producers) t.join();
std::this_thread::sleep_for(std::chrono::milliseconds(500));
logger.stop();
consumer.join();
std::cout << "\n所有日志已写入磁盘\n";
}
锁粒度优化要点:
- 临界区只保护共享数据,不保护耗时操作
- 日志写入在
unlock()之后执行,不阻塞生产者 condition_variable::wait的 lambda 检查唤醒条件,处理虚假唤醒- 批量处理(while 循环取完队列)减少锁竞争
易错场景与面试考点
易错场景
1. 忘记 notify
void produce(int val) {
std::lock_guard lock(mtx);
queue.push(val);
} // ❌ 忘记 cv.notify_one(),消费者可能永远等待
2. 条件变量虚假唤醒
// ❌ 用 if 而非 while
cv.wait(lock, []{ return ready; }); // ✅ 用 lambda 谓词自动处理
// 等价于
while (!ready) // ✅ 手动 while 循环
cv.wait(lock);
3. 锁的粒度不当
// ❌ 锁覆盖耗时操作
{
std::lock_guard lock(mtx);
auto data = queue.front(); queue.pop();
writeToDisk(data); // ❌ 耗时操作在锁内
}
// ✅ 先取出数据,再释放锁
std::string data;
{
std::lock_guard lock(mtx);
data = std::move(queue.front());
queue.pop();
}
writeToDisk(data); // ✅ 锁外操作
面试考点
| 考点 | 要点 |
|---|---|
| 死锁四条件 | 互斥、持有等待、不可抢占、循环等待——破坏任一即可预防 |
| 死锁预防 | std::lock 同时加锁、固定顺序、超时机制(try_lock_for) |
| 数据竞争 | 多线程无同步地访问同一内存且至少一个是写 |
| 条件变量 | wait 需 while 循环或用带谓词的 wait 重载 |
| 锁粒度 | 临界区尽量小,耗时操作放锁外 |
| 无锁编程 | atomic + CAS(compare_exchange)实现无锁栈/队列 |
once_flag | call_once 保证初始化仅执行一次(替代 double-checked locking) |