在现代c++并发编程中,std::thread和std::async是主要工具,其中std::thread用于底层线程管理,而std::async提供更高层次的异步任务封装。1. std::thread需要手动处理线程生命周期(join或detach)及数据同步;2. std::async配合std::future简化异步编程,自动处理线程管理和异常传递;3. 使用std::launch::async可确保任务在独立线程执行,避免延迟模式带来的串行化问题;4. 线程同步需借助互斥量(std::mutex)和条件变量(std::condition_variable)来保障数据安全与线程协作;5. 推荐优先使用std::async以减少复杂性,除非需要对线程进行细粒度控制。

现代C++中,使用线程库进行并发编程,主要围绕
std::thread和
std::async展开。简单来说,
std::thread提供的是更底层的线程创建和管理能力,你需要手动处理线程的生命周期(
join或
detach)和数据同步;而
std::async则是一种更高层次的抽象,它将异步任务的执行和结果获取封装起来,通常配合
std::future来简化异步编程,让你不必直接面对线程的细节,更侧重于任务本身。选择哪一个,往往取决于你对控制粒度的需求和对复杂性的容忍度。

解决方案
在C++11及更高版本中,
std::thread是启动新执行流的基础工具,而
std::async则提供了更高级的异步任务管理机制,通常是更推荐的选项,因为它能更好地处理异常和结果返回。
使用 std::thread
:
std::thread允许你直接创建和管理操作系统级别的线程。它接受一个可调用对象(函数、Lambda表达式、函数对象或成员函数)作为新线程的入口点。
立即学习“C++免费学习笔记(深入)”;

#include <iostream>
#include <thread>
#include <vector>
#include <numeric> // For std::iota
void do_work(int id) {
std::cout << "Thread " << id << " is working...\n";
// 模拟一些计算
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "Thread " << id << " finished.\n";
}
int main() {
std::cout << "Main thread starts.\n";
std::thread t1(do_work, 1); // 创建并启动线程t1
// 也可以使用lambda表达式
std::thread t2([](int data) {
std::cout << "Lambda thread processing data: " << data << "\n";
}, 42);
// 线程的生命周期管理至关重要
// 必须在线程对象销毁前调用 join() 或 detach()
t1.join(); // 等待t1完成
t2.join(); // 等待t2完成
std::cout << "All threads joined. Main thread ends.\n";
return 0;
}这里有个小细节,如果你不
join()或
detach()一个
std::thread对象,当它被销毁时,程序会直接崩溃,这是个常见的错误。
join()意味着主线程会等待子线程执行完毕,而
detach()则让子线程独立运行,主线程不再关心其结束。
使用 std::async
与 std::future
:
std::async是更高级的并发工具,它返回一个
std::future对象,通过这个
future你可以获取异步任务的结果,甚至等待任务完成。它甚至可以决定是否真的创建一个新线程,或者只是延迟执行任务。

#include <iostream>
#include <future> // For std::async and std::future
#include <chrono> // For std::chrono::milliseconds
int calculate_sum(int a, int b) {
std::cout << "Calculating sum of " << a << " and " << b << "...\n";
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
return a + b;
}
int main() {
std::cout << "Main thread launching async task.\n";
// 启动一个异步任务,并获取其future
// std::launch::async 强制在新线程中运行
// std::launch::deferred 延迟到 future 的 get() 或 wait() 时才运行
// 默认是 std::launch::async | std::launch::deferred,由系统决定
std::future<int> result_future = std::async(std::launch::async, calculate_sum, 10, 20);
std::cout << "Main thread continues its work...\n";
// 可以在这里做其他事情,不必等待
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Main thread waiting for async result...\n";
try {
int sum = result_future.get(); // 获取结果,会阻塞直到任务完成
std::cout << "Async task result: " << sum << "\n";
} catch (const std::exception& e) {
std::cerr << "An error occurred: " << e.what() << "\n";
}
std::cout << "Main thread ends.\n";
return 0;
}std::async的优势在于,它帮你处理了线程的创建、销毁、异常捕获和结果返回。你只需要关注你要执行的任务本身。
get()方法会阻塞当前线程直到异步任务完成并返回结果(或抛出异常)。
std::thread 的基础用法与生命周期管理:避免崩溃的必修课
谈到
std::thread,很多人可能初次尝试就会遇到一个问题:为什么我的程序会崩溃?这通常与线程的生命周期管理不当有关。一个
std::thread对象,在其生命周期结束时,如果它所代表的线程还没有
join(被主线程等待完成)或
detach(独立运行),那么程序就会调用
std::terminate,直接终止。这可不是什么友好的体验。
创建
std::thread对象很简单,可以传入一个普通函数、一个lambda表达式、一个函数对象,甚至是类的成员函数。比如:
#include <iostream>
#include <thread>
#include <string>
#include <chrono>
void simple_func(int count, const std::string& msg) {
for (int i = 0; i < count; ++i) {
std::cout << "Thread (func): " << msg << " - " << i << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
struct MyFunctor {
void operator()(double value) {
std::cout << "Thread (functor): Processing value " << value << std::endl;
}
};
class Worker {
public:
void do_something(const std::string& task_name) {
std::cout << "Thread (member): " << task_name << " is active." << std::endl;
}
};
int main() {
// 1. 使用普通函数
std::thread t1(simple_func, 3, "Hello from t1");
// 2. 使用lambda表达式
std::thread t2([](int id) {
std::cout << "Thread (lambda): ID " << id << " says hi!" << std::endl;
}, 101);
// 3. 使用函数对象
MyFunctor mf;
std::thread t3(mf, 3.14); // 注意:mf会被拷贝到线程内部
// 4. 使用类的成员函数 (需要传递对象实例的指针或引用)
Worker worker_obj;
std::thread t4(&Worker::do_something, &worker_obj, "Complex Task");
// 关键:管理线程生命周期
// join():等待线程完成。这是最常见的用法,确保子线程的工作在主线程继续前完成。
t1.join();
t2.join();
t3.join();
t4.join(); // 必须等待所有线程,否则程序可能在它们完成前结束
std::cout << "All threads have finished their work." << std::endl;
// 另一个选择是 detach()
// std::thread detached_t([](){
// std::cout << "I'm a detached thread, running independently!\n";
// std::this_thread::sleep_for(std::chrono::seconds(1));
// std::cout << "Detached thread finished.\n";
// });
// detached_t.detach(); // 一旦detach,你将无法再控制这个线程,也无法join它
// 注意:如果一个线程被detach,主程序退出时,如果它还在运行,可能会被操作系统强制终止,
// 或者继续运行直到完成,具体行为取决于操作系统。
// 一般来说,除非你明确知道自己在做什么,否则优先使用 join()。
return 0;
}参数传递时,要注意引用传递。如果你想在线程函数中修改主线程的变量,需要使用
std::ref包装,否则默认是值拷贝。
#include <iostream>
#include <thread>
#include <functional> // For std::ref
void increment_by_ref(int& counter) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
counter++;
std::cout << "Thread: counter is now " << counter << std::endl;
}
int main() {
int my_counter = 0;
// 错误:默认是值拷贝,线程内部修改的是拷贝
// std::thread t1(increment_by_ref, my_counter);
// 正确:使用std::ref进行引用传递
std::thread t1(increment_by_ref, std::ref(my_counter));
t1.join();
std::cout << "Main: final counter is " << my_counter << std::endl; // 应该显示 1
return 0;
}掌握
join()和
detach()是使用
std::thread的关键,它们决定了你如何处理线程的生命周期,以及主线程和子线程之间的协调关系。我个人更倾向于
join(),因为它提供了一种明确的同步点,让程序的行为更可预测。除非有非常明确的理由,否则
detach()可能会让调试变得复杂。
std::async 与 std::future:简化异步编程,但别忘了它的“懒惰”模式
std::async和
std::future的组合,简直是现代C++异步编程的瑞士军刀。它极大地简化了线程管理,尤其是当你只需要执行一个任务并获取其结果时。你不用再操心
join或
detach,也不用手动创建线程,甚至连异常处理都变得优雅了许多。
它的核心思想是:你提交一个任务(可调用对象),
std::async返回一个
std::future对象。这个
future对象就是你未来获取任务结果的凭证。当你调用
future.get()时,如果任务还没完成,它会阻塞直到任务完成并返回结果;如果任务已经完成,它会立即返回结果。更棒的是,如果任务中抛出了异常,
get()也会重新抛出这个异常,让你可以在主线程中统一处理。
#include <iostream>
#include <future>
#include <chrono>
#include <stdexcept> // For std::runtime_error
double divide(double numerator, double denominator) {
std::cout << "Async task: Performing division...\n";
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟计算耗时
if (denominator == 0) {
throw std::runtime_error("Division by zero!");
}
return numerator / denominator;
}
int main() {
std::cout << "Main: Launching division tasks.\n";
// 正常情况:异步执行并获取结果
std::future<double> f1 = std::async(divide, 10.0, 2.0);
// 异常情况:异步执行,但会抛出异常
std::future<double> f2 = std::async(divide, 10.0, 0.0);
std::cout << "Main: Doing other stuff while tasks run...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
// 获取第一个任务的结果
try {
double result1 = f1.get(); // 阻塞直到f1完成
std::cout << "Main: Result of 10.0 / 2.0 is: " << result1 << std::endl;
} catch (const std::exception& e) {
std::cerr << "Main: Error in f1: " << e.what() << std::endl;
}
// 获取第二个任务的结果
try {
double result2 = f2.get(); // 阻塞直到f2完成,并重新抛出异常
std::cout << "Main: Result of 10.0 / 0.0 is: " << result2 << std::endl;
} catch (const std::exception& e) {
std::cerr << "Main: Error in f2: " << e.what() << std::endl;
}
std::cout << "Main: All tasks processed.\n";
return 0;
}这里有个非常重要的点,就是
std::async的执行策略。它有一个可选的第一个参数,
std::launch。
std::launch::async
: 强制在新的线程中执行任务。std::launch::deferred
: 延迟执行任务。只有当future
的get()
或wait()
方法被调用时,任务才会在调用者的线程中执行。这就像一个懒惰的求值,任务根本不会异步运行。- 默认行为(不指定
std::launch
):是std::launch::async | std::launch::deferred
。这意味着系统会根据资源情况自行决定是立即启动新线程还是延迟执行。
这个默认行为有时候会让人困惑。我曾经就遇到过,写了一个
std::async,结果发现它根本没在新线程里跑,而是
get()的时候才执行,导致我以为的并发变成了串行。所以,如果你真的需要一个独立的线程来执行任务,我建议明确使用
std::launch::async。
std::future除了
get(),还有
wait()方法,它只等待任务完成而不获取结果。此外,
std::future_status可以用来检查任务的状态(是否完成、是否延迟等),配合
wait_for或
wait_until可以实现超时等待,这在需要响应性或避免无限期阻塞的场景中非常有用。
总的来说,
std::async是处理单个异步任务的首选,它隐藏了大部分线程管理的复杂性,让你可以专注于业务逻辑。但别忘了它的“懒惰”模式,必要时要显式指定执行策略。
线程同步与数据安全:互斥量和条件变量,并发编程的基石
一旦你开始在多个线程间共享数据,那么恭喜你,你已经踏入了并发编程最容易出错的雷区:数据竞争(Data Race)。没有适当的同步机制,多个线程同时读写同一个数据,其结果是不可预测的,这也就是所谓的未定义行为。为了避免这种混乱,我们需要互斥量(
std::mutex)和条件变量(
std::condition_variable)这样的工具。
互斥量(std::mutex
):独占访问的守护者
std::mutex是C++中最基本的同步原语,它提供了一种独占访问共享资源的方式。当一个线程锁住
mutex后,其他试图锁住同一个
mutex的线程都会被阻塞,直到第一个线程解锁。这保证了在任何时刻,只有一个线程可以访问被
mutex保护的代码段(临界区)。
然而,直接使用
std::mutex::lock()和
std::mutex::unlock()很容易出错,比如忘记解锁,或者在异常发生时没有解锁,导致死锁。因此,C++提供了RAII(资源获取即初始化)风格的锁,如
std::lock_guard和
std::unique_lock,它们在构造时加锁,在析构时自动解锁,极大地简化了安全编程。
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <numeric> // For std::iota
std::mutex mtx; // 全局互斥量,保护 shared_data
int shared_data = 0;
void increment_shared_data(int id) {
for (int i = 0; i < 1000; ++i) {
// 使用 std::lock_guard 确保互斥访问
// 当 lock_guard 对象超出作用域时,会自动解锁
std::lock_guard<std::mutex> lock(mtx);
shared_data++;
// std::cout << "Thread " << id << ": shared_data = " << shared_data << std::endl; // 频繁输出会影响性能
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment_shared_data, i);
}
for (auto& t : threads) {
t.join();
}
// 理论上,shared_data 应该是 10 * 1000 = 10000
std::cout << "Final shared_data: " << shared_data << std::endl; // 应该接近10000,如果没锁,会远小于
return 0;
}如果没有
std::lock_guard<std::mutex> lock(mtx);,
shared_data的最终值几乎不可能是10000,因为多个线程会同时读取、修改、写回,导致一些更新丢失。
std::lock_guard是日常使用中最方便的选择。
std::unique_lock则提供了更多灵活性,比如可以延迟加锁、手动解锁、尝试加锁等,适用于更复杂的场景。
条件变量(std::condition_variable
):线程间的信号与等待
互斥量解决了数据竞争问题,但它无法解决线程间的协作问题。例如,一个线程需要等待另一个线程完成某个任务或满足某个条件后才能继续执行。这时,条件变量就派上用场了。
std::condition_variable通常与
std::mutex和某个共享的布尔条件变量一起使用。一个线程等待某个条件满足,它会调用
wait()方法,这会自动释放互斥量并进入阻塞状态。当另一个线程改变了条件并通知(
notify_one()或
notify_all())条件变量时,等待的线程会被唤醒,并重新尝试获取互斥量,然后检查条件是否真的满足。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue> // 模拟生产者-消费者队列
std::mutex mtx_cv;
std::condition_variable cv;
std::queue<int> data_queue;
bool producer_finished = false; // 生产者是否已完成所有生产
void producer() {
for (int i = 0; i < 5; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产时间
{
std::lock_guard<std::mutex> lock(mtx_cv);
data_queue.push(i);
std::cout << "Producer: Produced " << i << std::endl;
} // 锁在这里释放
cv.notify_one(); // 通知一个等待的消费者
}
{
std::lock_guard<std::mutex> lock(mtx_cv);
producer_finished = true;
}
cv.notify_all(); // 生产完成后,通知所有可能还在等待的消费者
}
void consumer(int id) {
while (true) {
std::unique_lock<std::mutex> lock(mtx_cv); // unique_lock 允许在 wait 期间释放锁
// 等待条件:队列不为空 或者 生产者已完成
cv.wait(lock, [&]{ return !data_queue.empty() || producer_finished; });
if (data_queue.empty() && producer_finished) {
std::cout << "Consumer " << id << ": Producer finished and queue is empty. Exiting.\n";
break; // 退出循环
}
int data = data_queue.front();
data_queue.pop();
std::cout << "Consumer " << id << ": Consumed " << data << std::endl;
// 锁在这里自动释放,或者手动释放 lock.unlock();
}
}
int main() {
std::thread prod_t(producer);
std::thread cons1_t(consumer, 1);
std::thread cons2_t(consumer, 2);
prod_t.join();
cons1_t.join();
cons2_t.join();
std::cout << "Main: All threads finished.\n";
return 0;
}条件变量的
wait()方法通常接受一个lambda表达式作为谓词(predicate)。这个谓词会在
wait()被唤醒后自动检查条件是否满足。如果条件不满足,它会再次进入等待状态。这避免了“虚假唤醒”(spurious wakeups)的问题,即线程被唤醒但条件并未真正满足。
除了`std::mutex










