C++ 多线程与并发面试题
1. 进程和线程的区别?
答案:
- 进程资源分配的基本单位独立的地址空间进程间通信(IPC)开销大创建销毁开销大
- 线程CPU调度的基本单位共享进程的地址空间线程间通信简单(共享内存)创建销毁开销小
- 对比进程更安全,隔离性好线程更轻量,切换快线程共享资源,需要同步
- C++中的线程C++11引入std::thread跨平台的线程库RAII风格管理
2. 什么是线程安全?如何实现?
答案:
- 定义多线程访问时,程序行为正确不会出现数据竞争结果可预测
- 实现方法互斥锁(Mutex)保护临界区同一时间只有一个线程访问读写锁多个读者,一个写者提高并发度原子操作不可分割的操作无需加锁无锁数据结构使用CAS(Compare-And-Swap)高性能
- 线程安全的类
class ThreadSafeCounter {
mutable mutex mtx;
int value;
public:
void increment() {
lock_guard<mutex> lock(mtx);
value++;
}
int get() const {
lock_guard<mutex> lock(mtx);
return value;
}
};
3. 什么是死锁?如何避免?
答案:
- 死锁定义两个或多个线程互相等待对方释放资源导致程序永久阻塞
- 死锁的四个必要条件互斥:资源不能共享持有并等待:持有资源同时等待其他资源不可抢占:资源不能被强制释放循环等待:存在资源等待环路
- 避免死锁固定加锁顺序
// 总是先锁id小的
void transfer(Account& from, Account& to, int amount) {
if (&from < &to) {
lock_guard<mutex> lock1(from.mtx);
lock_guard<mutex> lock2(to.mtx);
// 转账
} else {
lock_guard<mutex> lock1(to.mtx);
lock_guard<mutex> lock2(from.mtx);
// 转账
}
}
- 使用std::lock同时锁定
void transfer(Account& from, Account& to, int amount) {
lock(from.mtx, to.mtx);
lock_guard<mutex> lock1(from.mtx, adopt_lock);
lock_guard<mutex> lock2(to.mtx, adopt_lock);
// 转账
}
- 使用超时机制
- 避免嵌套锁
4. mutex、lock_guard、unique_lock的区别?
答案:
- mutex基本互斥锁需要手动lock/unlock容易忘记解锁
mutex mtx; mtx.lock(); // 临界区 mtx.unlock();
- lock_guardRAII风格构造时加锁,析构时解锁不能手动解锁轻量级
{
lock_guard<mutex> lock(mtx);
// 临界区
} // 自动解锁
- unique_lock更灵活的RAII可以手动lock/unlock可以转移所有权可以延迟加锁配合条件变量使用
unique_lock<mutex> lock(mtx, defer_lock); // 做其他事 lock.lock(); // 临界区 lock.unlock(); // 做其他事
- 选择建议简单场景用lock_guard需要灵活控制用unique_lock条件变量必须用unique_lock
5. 什么是条件变量?如何使用?
答案:
- 定义线程间同步机制等待某个条件成立避免忙等待
- 基本使用
mutex mtx;
condition_variable cv;
queue<int> data_queue;
// 生产者
void producer() {
for (int i = 0; i < 10; i++) {
{
lock_guard<mutex> lock(mtx);
data_queue.push(i);
}
cv.notify_one(); // 通知消费者
}
}
// 消费者
void consumer() {
while (true) {
unique_lock<mutex> lock(mtx);
cv.wait(lock, []{ return !data_queue.empty(); });
int value = data_queue.front();
data_queue.pop();
lock.unlock();
// 处理value
}
}
- APIwait:等待通知notify_one:唤醒一个等待线程notify_all:唤醒所有等待线程
- 注意事项必须配合unique_lock使用使用谓词避免虚假唤醒修改条件前加锁
6. 什么是原子操作?
答案:
- 定义不可分割的操作不会被线程切换打断无需加锁
- std::atomic
#include <atomic>
atomic<int> counter(0);
void increment() {
counter++; // 原子操作
}
void decrement() {
counter--; // 原子操作
}
- 支持的操作load:读取store:写入exchange:交换compare_exchange:CAS操作fetch_add、fetch_sub等
- 内存序memory_order_relaxed:最宽松memory_order_acquire:获取语义memory_order_release:释放语义memory_order_seq_cst:顺序一致(默认)
- 使用场景简单的计数器标志位无锁数据结构
7. 什么是生产者-消费者模型?
答案:
- 模型描述生产者生产数据放入缓冲区消费者从缓冲区取数据消费缓冲区有限,需要同步
- 实现(使用条件变量)
class BoundedQueue {
queue<int> q;
mutex mtx;
condition_variable not_full;
condition_variable not_empty;
size_t capacity;
public:
BoundedQueue(size_t cap) : capacity(cap) {}
void produce(int item) {
unique_lock<mutex> lock(mtx);
not_full.wait(lock, [this]{ return q.size() < capacity; });
q.push(item);
not_empty.notify_one();
}
int consume() {
unique_lock<mutex> lock(mtx);
not_empty.wait(lock, [this]{ return !q.empty(); });
int item = q.front();
q.pop();
not_full.notify_one();
return item;
}
};
- 应用场景任务队列消息队列线程池
8. 什么是读者-写者问题?
答案:
- 问题描述多个读者可以同时读写者独占访问读写互斥
- 实现(使用shared_mutex)
#include <shared_mutex>
class SharedData {
mutable shared_mutex mtx;
int data;
public:
int read() const {
shared_lock<shared_mutex> lock(mtx); // 共享锁
return data;
}
void write(int value) {
unique_lock<shared_mutex> lock(mtx); // 独占锁
data = value;
}
};
- 策略读者优先:可能导致写者饥饿写者优先:可能导致读者饥饿公平策略:平衡读写
9. 什么是线程池?如何实现?
答案:
- 定义预先创建一组线程复用线程执行任务避免频繁创建销毁线程
- 简单实现
class ThreadPool {
vector<thread> workers;
queue<function<void()>> tasks;
mutex mtx;
condition_variable cv;
bool stop;
public:
ThreadPool(size_t threads) : stop(false) {
for (size_t i = 0; i < threads; 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 (thread& worker : workers)
worker.join();
}
};
- 优势减少线程创建开销控制并发数量提高响应速度
10. future和promise的使用?
答案:
- 定义future:异步操作的结果promise:设置future的值线程间传递数据
- 使用async
#include <future>
int compute(int x) {
return x * x;
}
future<int> result = async(launch::async, compute, 10);
// 做其他事
int value = result.get(); // 获取结果,阻塞
- 使用promise
void compute(promise<int> p, int x) {
p.set_value(x * x);
}
promise<int> p;
future<int> f = p.get_future();
thread t(compute, move(p), 10);
int value = f.get();
t.join();
- packaged_task
packaged_task<int(int)> task([](int x) { return x * x; });
future<int> result = task.get_future();
thread t(move(task), 10);
int value = result.get();
t.join();
- 优势简化异步编程异常传播避免手动同步
C++面试总结 文章被收录于专栏
本专栏系统梳理C++面试高频考点,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力。
查看5道真题和解析