1. C++多线程编程实战:从std::thread到异步并发

在现代 C++ 开发中,多线程编程已经成为提升程序性能、充分利用多核处理器能力的关键技术。从 C++11 标准开始,标准库正式引入了线程支持库,其中 std::thread 是最基础的线程抽象;而随着 C++20 引入协程、以及标准库中 std::asyncstd::future 等组件的完善,C++ 的并发编程模型也日趋丰富。本文将从 std::thread 入手,逐步深入到异步并发编程,帮助读者建立一套完整、可落地的多线程实战知识体系。

2. 线程基础:std::thread 的使用

std::thread 是 C++ 标准库提供的线程类,它封装了操作系统原生线程,并提供了跨平台的统一接口。使用 std::thread 创建线程非常简单,只需要传入一个可调用对象即可。

#include <iostream>
#include <thread>

void hello() {
    std::cout << "Hello from thread, id = " << std::this_thread::get_id() << std::endl;
}

int main() {
    std::thread t(hello);
    t.join();  // 等待线程结束
    return 0;
}

在上面的示例中,std::thread t(hello) 创建了一个执行 hello 函数的新线程,随后通过 t.join() 阻塞主线程,直到子线程执行完毕。除了 join(),还可以调用 detach() 让线程在后台独立运行,但需要注意:一旦 detach() 之后,线程对象将不再与该线程关联,也无法再通过该对象等待线程结束。

3. 线程传参与返回值

向线程函数传递参数时,所有参数都会按值复制到线程的存储空间中。如果需要传递引用,必须显式使用 std::ref 包装,否则即使函数签名是引用类型,实际传入的也是副本。

#include <iostream>
#include <thread>

void add(int a, int b, int& result) {
    result = a + b;
}

int main() {
    int sum = 0;
    std::thread t(add, 3, 4, std::ref(sum));
    t.join();
    std::cout << "sum = " << sum << std::endl;  // 输出 sum = 7
    return 0;
}

需要注意的是,std::thread 本身并不直接支持返回值。如果希望从线程中获取计算结果,通常的做法是通过引用参数、共享变量加锁,或者使用后面将要介绍的 std::asyncstd::future 机制。

4. 线程同步与互斥

当多个线程同时访问共享数据时,就会产生数据竞争(data race),进而导致未定义行为。C++ 标准库提供了 std::mutexstd::lock_guard 等工具来保护共享资源。

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

std::mutex mtx;
int counter = 0;

void increment(int times) {
    for (int i = 0; i < times; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        ++counter;
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i) {
        threads.emplace_back(increment, 10000);
    }
    for (auto& t : threads) {
        t.join();
    }
    std::cout << "counter = " << counter << std::endl;  // 输出 counter = 40000
    return 0;
}

std::lock_guard 采用 RAII 机制,在构造时自动加锁,在析构时自动解锁,即使发生异常也能保证锁被正确释放,从而避免死锁和资源泄漏。对于需要更精细控制的场景,可以使用 std::unique_lock,它支持手动加锁、解锁以及延迟加锁等操作。

5. 条件变量与线程通信

条件变量(std::condition_variable)用于在线程之间传递事件通知,它通常与互斥锁配合使用,实现「等待-通知」的同步模式。典型的生产者-消费者模型就是条件变量的经典应用场景。

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;

void producer() {
    for (int i = 0; i < 5; ++i) {
        {
            std::lock_guard<std::mutex> lock(mtx);
            data_queue.push(i);
            std::cout << "produced: " << i << std::endl;
        }
        cv.notify_one();
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [] { return !data_queue.empty(); });
        int value = data_queue.front();
        data_queue.pop();
        lock.unlock();
        std::cout << "consumed: " << value << std::endl;
        if (value == 4) break;
    }
}

int main() {
    std::thread p(producer);
    std::thread c(consumer);
    p.join();
    c.join();
    return 0;
}

在消费者线程中,cv.wait(lock, predicate) 会先释放锁,然后进入阻塞状态;当生产者调用 notify_one() 后,消费者被唤醒并重新获取锁,再检查谓词条件是否满足。使用带谓词的 wait 可以避免虚假唤醒(spurious wakeup)带来的问题。

6. 异步并发:std::async 与 std::future

与手动管理 std::thread 不同,std::async 提供了一种更高层的异步任务抽象。它会在合适的时机启动一个异步任务,并返回一个 std::future 对象,通过该对象可以获取任务的执行结果。

#include <iostream>
#include <future>

int compute(int a, int b) {
    return a * b;
}

int main() {
    std::future<int> result = std::async(std::launch::async, compute, 6, 7);
    std::cout << "result = " << result.get() << std::endl;  // 输出 result = 42
    return 0;
}

std::async 的第一个参数是启动策略:std::launch::async 表示强制在新线程中执行;std::launch::deferred 表示延迟到调用 get()wait() 时才在调用线程中执行;默认策略则允许实现自行选择。调用 future.get() 会阻塞当前线程,直到异步任务完成并返回结果。

7. std::packaged_task 与 std::promise

除了 std::async,C++ 还提供了 std::packaged_taskstd::promise 两种更灵活的异步工具。std::packaged_task 将一个可调用对象包装起来,并把其返回值与 std::future 关联;std::promise 则允许在线程之间手动传递值或异常。

#include <iostream>
#include <future>
#include <thread>

void set_value(std::promise<int> prom) {
    prom.set_value(100);
}

int main() {
    std::promise<int> prom;
    std::future<int> fut = prom.get_future();
    std::thread t(set_value, std::move(prom));
    std::cout << "value = " << fut.get() << std::endl;  // 输出 value = 100
    t.join();
    return 0;
}

需要注意的是,std::promisestd::packaged_task 都是不可复制、只能移动的对象,因此在传递给线程时需要使用 std::move。这种机制非常适合在复杂的任务调度场景中,把某个线程的计算结果安全地传递给另一个线程。

8. 原子操作与无锁编程

对于简单的计数器、标志位等场景,使用互斥锁可能带来不必要的性能开销。此时可以使用 std::atomic 原子类型,它基于硬件提供的原子指令实现,无需加锁即可保证操作的原子性。

#include <iostream>
#include <thread>
#include <atomic>
#include <vector>

std::atomic<int> counter{0};

void increment(int times) {
    for (int i = 0; i < times; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i) {
        threads.emplace_back(increment, 10000);
    }
    for (auto& t : threads) {
        t.join();
    }
    std::cout << "counter = " << counter.load() << std::endl;  // 输出 counter = 40000
    return 0;
}

原子操作还支持内存序(memory order)参数,用于控制操作的可见性和重排序约束。对于大多数场景,使用默认的 std::memory_order_seq_cst 即可保证正确性;只有在追求极致性能且对底层内存模型有深入理解时,才建议使用 relaxedacquirerelease 等更宽松的内存序。

9. 线程池与任务队列

频繁创建和销毁线程的开销较大,因此在处理大量短小任务时,通常会使用线程池来复用线程。下面给出一个简单的线程池实现,它基于任务队列和条件变量构建。

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <functional>
#include <vector>

class ThreadPool {
public:
    explicit ThreadPool(size_t threads) : stop(false) {
        for (size_t i = 0; i < threads; ++i) {
            workers.emplace_back([this] {
                while (true) {
                    std::function<void()> task;
                    {
                        std::unique_lock<std::mutex> lock(queue_mutex);
                        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::unique_lock<std::mutex> lock(queue_mutex);
            tasks.emplace(std::forward<F>(f));
        }
        cv.notify_one();
    }

    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            stop = true;
        }
        cv.notify_all();
        for (auto& worker : workers) {
            worker.join();
        }
    }

private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex queue_mutex;
    std::condition_variable cv;
    bool stop;
};

int main() {
    ThreadPool pool(4);
    for (int i = 0; i < 8; ++i) {
        pool.enqueue([i] {
            std::cout << "task " << i << " executed by thread "
                      << std::this_thread::get_id() << std::endl;
        });
    }
    return 0;
}

这个线程池在构造时创建固定数量的工作线程,并通过任务队列接收外部提交的任务。析构时设置停止标志并唤醒所有线程,等待它们处理完剩余任务后退出。在实际项目中,还可以在此基础上扩展支持带返回值的任务、任务优先级、动态扩容等能力。

10. 常见陷阱与最佳实践

多线程编程中容易踩坑的地方很多,这里总结几个最常见的陷阱和对应的最佳实践。

  • 数据竞争:多个线程同时读写同一变量而未加同步,会导致未定义行为。解决方法是使用互斥锁、原子变量或线程局部存储。
  • 死锁:多个线程以不同顺序获取多个锁时可能产生死锁。建议使用 std::lock 一次性锁定多个互斥量,或保证所有线程按相同顺序加锁。
  • 悬空引用:向线程传递局部变量的引用或指针,而该变量在线程执行前已被销毁。应确保线程访问的对象生命周期足够长,或按值传递。
  • 忘记 join 或 detachstd::thread 析构时如果线程仍可 join,会调用 std::terminate。务必在析构前明确调用 join()detach()
  • 过度加锁:锁粒度过大会降低并发性能。应尽量缩小临界区范围,只在必要时加锁。

本文从 std::thread 的基础用法出发,系统介绍了 C++ 多线程编程的核心内容,包括线程创建与传参、互斥锁与条件变量、异步任务(std::asyncstd::futurestd::packaged_taskstd::promise)、原子操作以及线程池的实现。掌握这些工具之后,读者可以根据实际场景选择合适的并发模型:简单任务优先考虑 std::async,需要精细控制线程生命周期时使用 std::thread,高并发短任务场景则适合引入线程池。多线程编程的难点不仅在于 API 的使用,更在于对数据竞争、死锁等并发问题的深刻理解,建议读者在实战中多写多练,逐步积累经验。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐