C++八股文(多线程与并发)
1. C++ 中的线程(std::thread)如何创建和管理?
1. 创建线程: 使用std::thread构造函数传入可调用对象(函数、lambda、函数对象),线程创建后立即开始执行,可以传递参数给线程函数,参数默认按值拷贝可以用std::ref传引用。
2. 线程管理: 必须调用join()等待线程结束或detach()分离线程,未join或detach的线程析构时会调用std::terminate,joinable()检查线程是否可join,get_id()获取线程ID,hardware_concurrency()获取硬件并发数。
3. 参数传递: 参数按值拷贝到线程内部避免悬空引用,使用std::ref传递引用,使用std::move转移所有权,注意线程函数的参数生命周期必须长于线程执行时间。
4. 注意事项: 线程不可拷贝只能移动,异常安全需要确保join或detach被调用,避免在析构函数中启动线程,使用RAII封装线程管理(如jthread C++20)。
// 创建和管理线程
void task(int n) { std::cout << "Task " << n << std::endl; }
std::thread t1(task, 42); // 函数+参数
std::thread t2([]{ /* lambda */ }); // lambda
t1.join(); // 等待线程结束
t2.detach(); // 分离线程
// RAII封装
class ThreadGuard {
std::thread& t;
public:
ThreadGuard(std::thread& t_) : t(t_) {}
~ThreadGuard() { if(t.joinable()) t.join(); }
};
2. C++ 中的 std::mutex 和 std::lock_guard 是如何工作的?
1. std::mutex工作原理: 互斥锁提供独占访问保护临界区,lock()获取锁(阻塞直到成功),unlock()释放锁,try_lock()尝试获取锁立即返回,同一线程重复lock会死锁。
2. std::lock_guard特点: RAII封装的锁管理器,构造时自动加锁析构时自动解锁,不可拷贝不可移动,适合简单的作用域锁定,无法手动unlock只能等待析构。
3. std::unique_lock特点: 更灵活的锁管理器,可以手动lock/unlock,支持延迟加锁和条件变量,可以移动但不可拷贝,可以提前释放锁或转移所有权。
4. 使用建议: 优先使用lock_guard简单高效,需要灵活控制时使用unique_lock,锁的粒度尽可能小减少竞争,避免在持有锁时调用外部函数防止死锁。
std::mutex mtx;
int shared_data = 0;
// lock_guard:自动管理
void safe_increment() {
std::lock_guard<std::mutex> lock(mtx);
++shared_data;
} // 自动解锁
// unique_lock:灵活控制
void flexible_lock() {
std::unique_lock<std::mutex> lock(mtx);
++shared_data;
lock.unlock(); // 提前解锁
// 做其他不需要锁的工作
lock.lock(); // 重新加锁
}
3. C++ 中如何使用 std::condition_variable 进行线程同步?
1. 基本概念: 条件变量用于线程间的通知机制,一个线程等待条件满足另一个线程通知条件改变,必须配合mutex和unique_lock使用,避免虚假唤醒需要在循环中检查条件。
2. 主要操作: wait()释放锁并等待通知,被唤醒后重新获取锁,notify_one()唤醒一个等待线程,notify_all()唤醒所有等待线程,wait_for()和wait_until()支持超时等待。
3. 虚假唤醒: 线程可能在没有notify的情况下被唤醒,必须使用while循环检查条件而不是if,wait()的第二个参数可以传入谓词自动处理虚假唤醒。
4. 使用模式: 生产者通知消费者数据就绪,任务队列的等待和通知,线程池的任务分发,实现信号量和屏障等同步原语。
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
// 等待线程
void wait_thread() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // 自动处理虚假唤醒
// 条件满足,继续执行
}
// 通知线程
void notify_thread() {
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
} // 先解锁再通知,避免等待线程立即阻塞
cv.notify_one();
}
4. C++ 中的原子操作(std::atomic)如何使用?
1. 基本概念: 原子操作保证操作的不可分割性,无需加锁实现线程安全,硬件级别的支持性能高于互斥锁,适用于简单的共享变量(计数器、标志位)。
2. 支持类型: 整数类型(int、long等)、指针类型、bool类型,自定义类型需要满足trivially copyable,常用操作有load、store、exchange、compare_exchange。
3. 内存序: memory_order_relaxed(最弱无同步),memory_order_acquire/release(获取释放语义),memory_order_seq_cst(顺序一致性默认最强),根据需求选择合适的内存序优化性能。
4. 使用场景: 无锁计数器、标志位、自旋锁实现、无锁数据结构、引用计数(shared_ptr内部使用),注意原子操作只保证单个变量的原子性不保证多个变量的一致性。
std::atomic<int> counter(0); std::atomic<bool> flag(false); // 原子操作 counter++; // 原子递增 counter.fetch_add(1); // 显式原子加法 int old = counter.exchange(100); // 原子交换 // CAS操作 int expected = 10; bool success = counter.compare_exchange_strong(expected, 20); // 如果counter==10则设为20返回true,否则expected被更新为counter的值 // 内存序优化 counter.store(42, std::memory_order_relaxed); int val = counter.load(std::memory_order_acquire);
5. C++ 中如何避免死锁?
1. 死锁条件: 互斥(资源独占)、持有并等待(持有资源同时等待其他资源)、不可抢占(资源不能被强制释放)、循环等待(线程间形成环形等待链),四个条件同时满足才会死锁。
2. 避免策略: 按固定顺序获取锁避免循环等待,使用std::lock同时获取多个锁,使用try_lock尝试获取失败则释放已持有的锁,设置超时机制避免无限等待。
3. 锁的层次: 为锁分配优先级总是按优先级顺序获取,使用lock_guard和unique_lock的RAII特性自动释放,避免在持有锁时调用外部函数或回调,减小锁的粒度和持有时间。
4. 检测和恢复: 使用死锁检测工具(ThreadSanitizer),设计时避免嵌套锁,使用无锁数据结构,实在需要多个锁时使用std::scoped_lock(C++17)一次性获取所有锁。
std::mutex m1, m2;
// 错误:可能死锁
void thread1() {
std::lock_guard<std::mutex> lock1(m1);
std::lock_guard<std::mutex> lock2(m2); // 线程2可能持有m2等待m1
}
// 正确:使用std::lock同时获取
void thread2() {
std::scoped_lock lock(m1, m2); // C++17,原子获取多个锁
}
// 正确:固定顺序
void thread3() {
std::lock_guard<std::mutex> lock1(m1); // 总是先m1后m2
std::lock_guard<std::mutex> lock2(m2);
}
6. 如何实现线程池?
1. 基本组件: 任务队列存储待执行任务,工作线程从队列取任务执行,互斥锁保护任务队列,条件变量通知线程有新任务,停止标志控制线程池关闭。
2. 工作流程: 初始化时创建固定数量的工作线程,线程循环等待任务队列有任务,提交任务时加入队列并通知等待线程,关闭时设置停止标志并通知所有线程退出。
3. 任务提交: 使用std::function存储任意可调用对象,使用std::packaged_task包装任务获取future,支持返回值和异常传递,使用完美转发传递参数。
4. 优化策略: 动态调整线程数量适应负载,任务优先级队列,线程亲和性绑定CPU核心,任务窃取(work stealing)平衡负载,避免任务队列无限增长设置上限。
class ThreadPool {
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex mtx;
std::condition_variable cv;
bool stop = false;
public:
ThreadPool(size_t threads) {
for(size_t i = 0; i < threads; ++i) {
workers.emplace_back([this] {
while(true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this]{ return stop || !tasks.empty(); });
if(stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
template<class F>
void enqueue(F&& f) {
{
std::lock_guard<std::mutex> lock(mtx);
tasks.emplace(std::forward<F>(f));
}
cv.notify_one();
}
~ThreadPool() {
{
std::lock_guard<std::mutex> lock(mtx);
stop = true;
}
cv.notify_all();
for(auto& worker : workers) worker.join();
}
};
7. 如何使用 C++ 实现生产者-消费者模型?
1. 基本结构: 共享缓冲区(队列)存储数据,生产者线程生产数据放入缓冲区,消费者线程从缓冲区取数据消费,互斥锁保护缓冲区,条件变量实现等待和通知。
2. 同步机制: 缓冲区满时生产者等待,缓冲区空时消费者等待,生产者放入数据后通知消费者,消费者取出数据后通知生产者,使用两个条件变量分别通知生产者和消费者。
3. 实现要点: 使用循环队列或std::queue作为缓冲区,设置缓冲区大小限制防止无限增长,正确处理多生产者多消费者场景,关闭时通知所有线程退出。
4. 优化方向: 批量生产和消费减少锁竞争,使用无锁队列提高性能,分离读写锁减少阻塞,使用信号量代替条件变量简化逻辑。
template<typename T>
class BoundedQueue {
std::queue<T> queue;
std::mutex mtx;
std::condition_variable not_full, not_empty;
size_t capacity;
public:
BoundedQueue(size_t cap) : capacity(cap) {}
void produce(T item) {
std::unique_lock<std::mutex> lock(mtx);
not_full.wait(lock, [this]{ return queue.size() < capacity; });
queue.push(std::move(item));
not_empty.notify_one();
}
T consume() {
std::unique_lock<std::mutex> lock(mtx);
not_empty.wait(lock, [this]{ return !queue.empty(); });
T item = std::move(queue.front());
queue.pop();
not_full.notify_one();
return item;
}
};
8. C++ 中的 std::async 如何创建异步任务?
1. 基本用法: std::async启动异步任务返回std::future获取结果,自动管理线程创建和销毁,支持函数、lambda、函数对象,参数传递方式与std::thread相同。
2. 启动策略: std::launch::async强制创建新线程异步执行,std::launch::deferred延迟执行直到调用get或wait,默认策略由实现决定可能异步或延迟,使用async策略确保真正异步。
3. 获取结果: future.get()阻塞等待结果只能调用一次,future.wait()等待完成不获取结果,future.wait_for()超时等待,异常会在get时重新抛出。
4. 使用场景: 简单的异步任务不需要手动管理线程,并行计算多个独立任务,异步IO操作,比线程池简单但灵活性较低,注意future析构时会阻塞等待任务完成。
// 创建异步任务
auto future = std::async(std::launch::async, [](int n) {
return n * n;
}, 42);
int result = future.get(); // 阻塞等待结果
// 多个异步任务
auto f1 = std::async(std::launch::async, task1);
auto f2 = std::async(std::launch::async, task2);
auto r1 = f1.get();
auto r2 = f2.get();
// 异常处理
auto f = std::async([]{ throw std::runtime_error("error"); });
try {
f.get(); // 异常在这里重新抛出
} catch(const std::exception& e) {}
9. C++ 中的线程局部存储(Thread Local Storage)是什么?
1. 基本概念: thread_local关键字声明的变量每个线程有独立副本,线程间互不影响无需同步,变量生命周期与线程相同,线程创建时初始化线程结束时销毁。
2. 使用场景: 线程特定的缓存或状态,避免锁竞争的性能优化,线程ID或线程名称存储,随机数生成器(每线程独立种子),errno等线程安全的全局变量。
3. 实现细节: 编译器和运行时支持,每个线程有独立的TLS区域,访问开销略高于普通变量但远低于锁,支持POD类型和有构造析构的类型,静态和非静态成员都可以是thread_local。
4. 注意事项: 过度使用会增加内存占用(每线程一份),初始化顺序可能不确定,不能用于线程间通信,析构顺序可能影响其他TLS变量。
// 线程局部变量
thread_local int thread_id = 0;
thread_local std::string thread_name;
void worker(int id) {
thread_id = id; // 每个线程独立
thread_name = "Worker-" + std::to_string(id);
// 使用thread_id和thread_name
}
// 线程局部缓存
thread_local std::unordered_map<int, Data> cache;
Data get_data(int key) {
if(cache.count(key)) return cache[key]; // 无锁访问
Data data = load_from_db(key);
cache[key] = data;
return data;
}
10. C++ 中如何实现无锁队列?
1. 基本原理: 使用原子操作(CAS)代替锁实现线程安全,通过循环重试处理竞争,ABA问题需要使用版本号或标记指针解决,性能高于加锁队列但实现复杂。
2. 单生产者单消费者: 使用两个原子索引(head和tail),生产者更新tail消费者更新head,无竞争时性能最优,可以使用环形缓冲区实现。
3. 多生产者多消费者: 使用CAS操作原子更新head和tail,需要处理ABA问题,使用hazard pointer或epoch-based回收管理内存,实现复杂但性能优于锁。
4. 实现难点: 内存回收(节点何时安全删除),ABA问题(指针被释放后重新分配到相同地址),内存序选择(性能和正确性平衡),需要深入理解内存模型。
// 简单的无锁队列(单生产者单消费者)
template<typename T>
class LockFreeQueue {
struct Node { T data; std::atomic<Node*> next; };
std::atomic<Node*> head;
std::atomic<Node*> tail;
public:
void enqueue(T value) {
Node* node = new Node{value, nullptr};
Node* prev = tail.exchange(node, std::memory_order_acq_rel);
prev->next.store(node, std::memory_order_release);
}
bool dequeue(T& result) {
Node* node = head.load(std::memory_order_acquire);
Node* next = node->next.load(std::memory_order_acquire);
if(next == nullptr) return false;
result = next->data;
head.store(next, std::memory_order_release);
delete node; // 简化版,实际需要安全回收
return true;
}
};
11. C++ 中的读写锁(std::shared_mutex)是什么?
1. 基本概念: 允许多个读者同时访问或一个写者独占访问,读多写少场景性能优于互斥锁,读者之间不互斥写者与所有线程互斥,C++17引入std::shared_mutex。
2. 使用方式: shared_lock用于读操作(共享锁),unique_lock或lock_guard用于写操作(独占锁),多个shared_lock可以同时持有,unique_lock会等待所有shared_lock释放。
3. 性能特点: 读多写少时性能显著优于mutex,写操作频繁时性能可能不如mutex(写者饥饿问题),实现开销略高于普通mutex,适合缓存、配置等读多写少场景。
4. 注意事项: 避免写者饥饿(读者过多导致写者长时间等待),不支持锁升级(shared_lock不能升级为unique_lock),递归加锁会死锁,C++14可用std::shared_timed_mutex支持超时。
std::shared_mutex rw_mutex;
std::map<int, std::string> cache;
// 读操作:共享锁
std::string read(int key) {
std::shared_lock<std::shared_mutex> lock(rw_mutex);
return cache[key]; // 多个读者可以同时访问
}
// 写操作:独占锁
void write(int key, std::string value) {
std::unique_lock<std::shared_mutex> lock(rw_mutex);
cache[key] = value; // 独占访问
}
12. C++ 中如何实现条件同步机制?
1. 条件变量: 使用std::condition_variable实现等待和通知,配合mutex和unique_lock使用,wait释放锁并等待notify唤醒后重新获取锁,适合复杂的条件同步。
2. 信号量: C++20引入std::counting_semaphore和std::binary_semaphore,acquire减少计数release增加计数,计数为0时阻塞,适合资源计数和限流场景。
3. 屏障: C++20引入std::barrier和std::latch,barrier可重用多个阶段同步,latch一次性倒计时到0,适合多线程分阶段执行和等待所有线程完成。
4. 原子标志: 使用std::atomic<bool>实现简单的标志位同步,配合自旋等待或sleep,适合简单的通知机制,性能高但CPU占用高。
// 条件变量
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
cv.wait(lock, []{ return ready; });
cv.notify_one();
// 信号量(C++20)
std::counting_semaphore<10> sem(3); // 初始计数3
sem.acquire(); // 计数-1
sem.release(); // 计数+1
// 屏障(C++20)
std::barrier sync_point(3); // 3个线程同步
sync_point.arrive_and_wait(); // 等待所有线程到达
// 原子标志
std::atomic<bool> flag(false);
while(!flag.load()) std::this_thread::yield();
本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。
查看16道真题和解析