C++ 多线程与线程同步面试题大全

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: 什么是死锁?如何避免?

死锁条件(四个同时满足):

  1. 互斥
  2. 持有并等待
  3. 不可剥夺
  4. 循环等待

避免方法

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: 为什么需要线程池?

  1. 减少开销:避免频繁创建/销毁线程
  2. 资源控制:限制并发线程数
  3. 任务队列:管理待执行任务

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
频繁创建线程 线程池
相关推荐
cooldream20096 小时前
Vim 报错 E325:swap 文件冲突的原理、处理流程与彻底避免方案
linux·编辑器·vim
i建模6 小时前
在 Rocky Linux 上安装轻量级的 XFCE 桌面
linux·运维·服务器
zhuqiyua6 小时前
第一次课程家庭作业
c++
只是懒得想了6 小时前
C++实现密码破解工具:从MD5暴力破解到现代哈希安全实践
c++·算法·安全·哈希算法
若风的雨6 小时前
WC (Write-Combining) 内存类型优化原理
linux
YMWM_6 小时前
不同局域网下登录ubuntu主机
linux·运维·ubuntu
码农水水6 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
zmjjdank1ng6 小时前
restart与reload的区别
linux·运维
哼?~6 小时前
进程替换与自主Shell
linux
m0_736919107 小时前
模板编译期图算法
开发语言·c++·算法