C++ 并发

C++ 并发编程是现代软件开发中的核心技术,主要用于利用多核处理器提升程序性能。C++11 及后续标准引入了完善的并发库(<thread><mutex><condition_variable> 等),使开发者能更安全地编写多线程程序。

1、std::thread

std::thread 是 C++11 引入的线程类,用于创建和管理线程。

1.1 基本使用

#include <thread>
#include <iostream>
using namespace std;

// 线程函数:普通函数
void thread_func(int id) {
    cout << "Thread " << id << " 运行" << endl;
}

int main() {
    // 创建线程(传入函数和参数)
    thread t1(thread_func, 1); 
    thread t2([](int id) {  // 线程函数:lambda表达式
        cout << "Thread " << id << " 运行" << endl;
    }, 2);

    // 等待线程结束(必须调用join()或detach(),否则程序崩溃)
    t1.join();  // 主线程阻塞,等待t1完成
    t2.join();  // 主线程阻塞,等待t2完成

    // 或分离线程(线程独立运行,主线程不等待)
    // t1.detach(); 
    // t2.detach();

    return 0;
}

1.2 关键特性

  • join():主线程阻塞等待子线程完成,回收线程资源。
  • detach():将线程与 std::thread 对象分离,线程后台运行,由系统自动回收资源(需确保线程访问的资源生命周期足够长)。
  • 线程对象必须被移动:std::thread 不可拷贝,只能移动(thread t2 = move(t1);)。

2、互斥锁

互斥锁(Mutex)用于保护共享资源,确保同一时间只有一个线程访问资源,避免数据竞争。

多个线程同时读写共享数据会导致数据竞争,结果是未定义行为。

  • std::mutex:基础互斥锁,提供 lock()(加锁)和 unlock()(解锁)方法(需手动配对,易出错)。
  • std::lock_guard:RAII 风格的锁管理,构造时自动加锁,析构时自动解锁(推荐,避免忘记解锁)。
  • std::unique_lock:更灵活的锁管理,支持手动 lock()/unlock()、超时等待等(适合条件变量)。

2.1 std::mutex

如果临界区中抛出异常,unlock() 可能不会被调用,导致死锁。

#include <thread>
#include <mutex>
#include <iostream>

int shared_data = 0;
std::mutex data_mutex; // 用于保护 shared_data

void increment() {
    for (int i = 0; i < 100000; ++i) {
        data_mutex.lock();   // 上锁
        ++shared_data;       // 临界区 (Critical Section)
        data_mutex.unlock(); // 解锁
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "Final value: " << shared_data << std::endl; // 总是 200000
    return 0;
}

2.2 lock_guard

这是推荐的做法,它在其作用域内自动管理锁的生命周期。

#include <mutex>
#include <vector>
#include <thread>

vector<int> shared_data;
mutex mtx;  // 保护shared_data的互斥锁

void add_data(int val) {
    lock_guard<mutex> lock(mtx);  // 构造时加锁,析构时解锁(离开作用域)
    shared_data.push_back(val);   // 安全访问共享资源
}

int main() {
    thread t1(add_data, 1);
    thread t2(add_data, 2);
    t1.join();
    t2.join();
    // shared_data 安全存储 [1,2] 或 [2,1]
    return 0;
}

2.3 std::unique_lock

std::lock_guard 更灵活(但开销稍大),可以延迟上锁、手动解锁、转移所有权。

void flexible_function() {
    std::unique_lock<std::mutex> ulock(data_mutex, std::defer_lock); // 延迟上锁
    // ... 做一些不需要锁的操作 ...
    ulock.lock(); // 现在需要锁了,手动上锁
    // ... 操作共享数据 ...
    ulock.unlock(); // 可以手动提前解锁
    // ... 更多操作 ...
    // ulock 析构时,如果还持有锁,会自动解锁
}

2.4 条件变量(std::condition_variable)

条件变量用于线程间通信,使线程能等待某个条件满足后再继续执行(如生产者 - 消费者模型)。

  • wait(lock, predicate):阻塞线程,释放锁并等待通知;被唤醒后重新获取锁,检查条件是否满足(不满足则继续等待)。
  • notify_one():唤醒一个等待的线程。
  • notify_all():唤醒所有等待的线程。
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>

std::queue<int> data_queue;
std::mutex queue_mutex;
std::condition_variable data_cond;

void data_preparation_thread() {
    int data = 0;
    while (true) {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        {
            std::lock_guard<std::mutex> lk(queue_mutex);
            data_queue.push(++data);
            std::cout << "Prepared data: " << data << std::endl;
        } // lock_guard 超出作用域,锁释放
        data_cond.notify_one(); // 通知一个等待的消费者线程
    }
}

void data_processing_thread() {
    while (true) {
        std::unique_lock<std::mutex> lk(queue_mutex);
        // 等待条件满足:Lambda 函数返回 true 时才继续,否则释放锁并等待通知
        data_cond.wait(lk, [] { return !data_queue.empty(); }); 
        // 被 notify 后,重新获取锁,并检查条件

        int data = data_queue.front();
        data_queue.pop();
        lk.unlock(); // 处理数据不需要锁,提前解锁

        std::cout << "Processed data: " << data << std::endl;
        // 处理数据...
    }
}

int main() {
    std::thread t1(data_preparation_thread);
    std::thread t2(data_processing_thread);
    t1.join();
    t2.join();
}

2.5 原子操作 (std::atomic)

std::atomic 提供无锁的原子操作,用于简单的计数器、标志位等场景,性能优于锁(无需上下文切换)。

#include <atomic>

atomic<int> counter(0);  // 原子计数器(线程安全)

void increment() {
    for (int i = 0; i < 1000; ++i) {
        counter++;  // 原子操作,无数据竞争
    }
}

int main() {
    thread t1(increment);
    thread t2(increment);
    t1.join();
    t2.join();
    cout << "计数器结果: " << counter << endl;  // 必然是2000(无竞争)
    return 0;
}

3、异步

3.1 std::async 和 std::future

std::async 用于启动异步任务,返回 std::future 对象,通过 future 获取任务结果(避免手动管理线程)。

#include <future>
#include <iostream>

int calculate() {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return 42;
}

int main() {
    // 启动一个异步任务
    // std::launch::async: 强制在新线程运行
    // std::launch::deferred: 延迟计算,直到调用 get() 时才在当前线程运行
    std::future<int> result_future = std::async(std::launch::async, calculate);

    std::cout << "Doing other work..." << std::endl;

    // 获取结果。如果计算未完成,会阻塞等待。
    int result = result_future.get(); 
    std::cout << "The answer is: " << result << std::endl;

    // result_future.get() 只能调用一次!
    return 0;
}

3.2 std::promise 和 std::future

用于在线程之间传递结果,提供更精细的控制。

void do_work(std::promise<int> result_promise) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    result_promise.set_value(100); // 设置结果值
    // 如果发生异常: result_promise.set_exception(std::current_exception());
}

int main() {
    std::promise<int> prom;
    std::future<int> fut = prom.get_future(); // 获取与 promise 关联的 future

    std::thread t(do_work, std::move(prom)); // 将 promise 移动到新线程

    int result = fut.get(); // 阻塞直到 promise 设置值
    std::cout << "Result: " << result << std::endl;

    t.join();
    return 0;
}

4、常见问题

  1. 什么是数据竞争 (Data Race)?如何避免?
    当两个或多个线程在没有同步的情况下并发访问同一个内存位置,并且至少有一个是写操作时,就会发生数据竞争。其结果是不确定的,是未定义行为。
    避免方法

    • 使用互斥锁 (std::mutex) 保护共享数据。
    • 将共享数据改为原子变量 (std::atomic)。
    • 重新设计程序,避免共享(例如,每个线程处理自己的数据副本,最后合并)。
  2. std::lock_guardstd::unique_lock 有什么区别?

核心区别在于灵活性

  • std::lock_guard 是轻量级锁管理工具,仅支持 “构造时锁定、析构时释放”,适合简单场景,性能更优;
  • std::unique_lock 支持延迟锁定、超时锁定、临时释放锁等高级操作,适合复杂同步场景(如与条件变量配合),但性能略差(维护额外状态)。
  1. 什么是死锁 (Deadlock)?如何预防?
    死锁是指两个或多个线程相互等待对方持有的资源,导致所有线程都无法继续执行的状态。
    产生条件(四个必要条件,缺一不可):

    1. 互斥:资源一次只能被一个线程持有。

    2. 持有并等待:线程持有一些资源,同时请求其他线程持有的资源。

    3. 不可剥夺:资源只能由持有它的线程主动释放。

    4. 循环等待:存在一个线程资源的循环等待链。

    预防策略

    1. 按固定顺序上锁:所有线程以相同的全局顺序获取锁(例如,总是先锁 mutex A,再锁 mutex B)。

    2. 使用 std::lock() 函数:它可以一次性锁定多个互斥量,且不会产生死锁(内部使用死锁避免算法)。

    std::mutex mutex1, mutex2;
    void safe_function() {
        std::lock(mutex1, mutex2); // 同时锁住两个,避免死锁
        std::lock_guard<std::mutex> lk1(mutex1, std::adopt_lock); // 接管已锁的mutex1
        std::lock_guard<std::mutex> lk2(mutex2, std::adopt_lock); // 接管已锁的mutex2
        // ...
    }
    
    1. 避免嵌套锁,或者使用超时机制 (std::timed_mutex)。
  2. 什么是虚假唤醒?std::condition_variable::wait 为什么要用循环检查条件?
    虚假唤醒是指等待中的线程在没有收到任何通知的情况下被操作系统唤醒。这是为了性能考虑,但会导致程序错误。
    解决方法:始终在循环中检查等待条件。

    data_cond.wait(lk, [] { return !data_queue.empty(); });
    // 这等价于:
    // while (!predicate()) { // 循环检查!
    //     data_cond.wait(lk);
    // }
    

    Lambda 函数(谓词)确保了即使发生虚假唤醒,线程也会再次检查条件是否真正满足,如果不满足会继续等待。

  3. std::async 的启动策略 std::launch::asyncstd::launch::deferred 有什么区别?

    • std::launch::async异步执行。强制要求在新创建的线程上执行任务。
    • std::launch::deferred延迟执行。任务不会立即执行,而是延迟到对返回的 std::future 调用 get()wait() 时,在调用者的线程中同步执行
    • 如果不指定策略,标准允许实现自由选择,这可能导致不确定性。最佳实践是显式指定策略
  4. 什么时候该用 std::atomic,什么时候该用 std::mutex

    • 使用 std::atomic:当你要保护的数据是简单的基本类型(如 int, bool, 指针)并且操作是单一的(读、写、递增、交换等)时。性能开销远小于互斥锁。
    • 使用 std::mutex
    • 当你要保护的数据是复杂的结构(如 std::vector, std::map)。
      • 当你需要执行一组操作临界区)来保持数据的不变性时。原子操作无法保证多个操作的整体原子性。
      • 例如,检查一个 std::map 中是否存在某个键,如果不存在则插入,这个操作必须用互斥锁保护,原子操作无法完成。
  5. 如何实现一个线程安全的单例模式?
    使用 Meyers' Singleton,它依靠静态局部变量的初始化在 C++11 及以后是线程安全的这一特性。

    class Singleton {
    public:
        static Singleton& getInstance() {
            static Singleton instance; // C++11保证此初始化是线程安全的
            return instance;
        }
    
        // 删除拷贝构造函数和赋值运算符以确保唯一性
        Singleton(const Singleton&) = delete;
        Singleton& operator=(const Singleton&) = delete;
    
        void doSomething() { /* ... */ }
    
    private:
        Singleton() = default; // 私有构造函数
        ~Singleton() = default;
    };
    
    // 使用
    Singleton::getInstance().doSomething();
    

    在 C++11 之前,需要使用双重检查锁定模式(Double-Checked Locking Pattern),但实现起来非常复杂且容易出错。Meyers' Singleton 是现代 C++ 中的最佳实践。

  6. 如何设计一个线程池?
    线程池是管理多个线程的对象,避免频繁创建/销毁线程的开销,核心组件包括:

    1. 工作线程:预先创建的线程,循环等待任务。
    2. 任务队列:存储待执行的任务(如 std::queue<std::function<void()>>)。
    3. 互斥锁:保护任务队列的线程安全。
    4. 条件变量:通知工作线程有新任务。

    简化实现思路

    class ThreadPool {
    private:
        vector<thread> workers;    // 工作线程
        queue<function<void()>> tasks;  // 任务队列
        mutex mtx;
        condition_variable cv;
        bool stop;  // 停止标志
    
        // 工作线程函数:循环取任务执行
        void worker() {
            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();  // 执行任务
            }
        }
    
    public:
        // 构造函数:创建n个工作线程
        ThreadPool(size_t n) : stop(false) {
            for (size_t i = 0; i < n; ++i) {
                workers.emplace_back(&ThreadPool::worker, this);
            }
        }
    
        // 提交任务
        template<class F>
        void submit(F&& f) {
            {
                lock_guard<mutex> lock(mtx);
                tasks.emplace(std::forward<F>(f));
            }
            cv.notify_one();  // 通知工作线程
        }
    
        // 析构函数:停止所有线程
        ~ThreadPool() {
            {
                lock_guard<mutex> lock(mtx);
                stop = true;
            }
            cv.notify_all();  // 唤醒所有线程
            for (auto& w : workers) {
                w.join();  // 等待线程结束
            }
        }
    };
    
  7. 条件变量(condition_variable)的 wait() 为什么需要传入 unique_lock

    wait() 需要在阻塞时释放锁(让其他线程有机会修改条件),被唤醒后重新获取锁(确保条件的安全性)。unique_lock 支持手动 lock()/unlock(),而 lock_guard 不支持手动解锁,因此 wait() 必须使用 unique_lock

  8. std::async 的三种启动策略有什么区别?

    std::async 有三种启动策略(C++11 定义):

    • std::launch::async:立即创建新线程执行任务,任务与主线程并行。
    • std::launch::deferred:任务延迟执行,直到调用 future::get()wait() 时,在当前线程同步执行。
    • std::launch::async | std::launch::deferred(默认):由系统决定策略(通常倾向于 async,但不保证)。
  9. std::unique_lock 为什么支持移动语义但不支持复制?

    std::unique_lock 管理的是互斥锁的独占所有权(同一时间只能有一个 unique_lock 控制锁):

    • 移动语义(std::move)可以转移所有权(从一个 unique_lock 到另一个),符合 “独占” 特性;
    • 复制会导致多个 unique_lock 同时管理同一把锁,违反 “独占” 原则(可能导致重复解锁或解锁未锁定的锁),因此被禁止。
posted @ 2025-09-17 17:48  xclic  阅读(31)  评论(0)    收藏  举报