C++多线程编程实战:std::thread与std::async核心差异与选型指南
1. 项目概述为什么我们需要在C里“搞”多线程干了这么多年C我越来越觉得单线程的程序就像一条单车道车再多也只能一辆一辆过。而现代CPU动不动就8核、16核让这么多核心闲着只跑一个线程简直是暴殄天物。无论是处理海量数据、构建高响应桌面应用还是开发游戏服务器多线程并发编程都是绕不开的坎。今天我们不谈那些晦涩的底层API就聚焦在C11标准引入的两个“明星选手”std::thread和std::async。它们让C的多线程编程从“手工作坊”进入了“标准化生产”时代。简单说std::thread给你最直接的线程控制权像一个手动挡汽车怎么开、何时换挡、何时刹车你说了算。而std::async则更像一个自动挡甚至带自动驾驶辅助你告诉它“去这个地方”它帮你管理线程的创建、执行乃至结果的获取。选择哪一个取决于你是想要极致的控制还是追求开发的便捷与安全。这篇文章我会结合我踩过的无数个坑带你彻底搞懂这两者的核心差异、适用场景并给出能直接“抄作业”的代码范例和避坑指南。无论你是刚接触并发的新手还是想系统梳理知识的老鸟相信都能有所收获。2. 核心概念扫盲线程、并发与并行在深入代码之前我们必须统一语言。很多人会把“并发”和“并行”混为一谈但在C多线程的语境下区分它们至关重要。并发指的是在一段时间内多个任务都在向前推进。注意是“一段时间内”而不是“同一时刻”。单核CPU通过时间片轮转让多个线程交替执行宏观上看起来像是同时运行这就是并发。它的核心目标是提高系统的响应能力和资源利用率比如一个GUI程序主线程处理用户交互后台线程进行文件下载用户不会感到卡顿。并行则强调在同一时刻有多个任务在同时执行。这通常需要多核CPU的支持。每个核心真正物理上同时运行一个线程。它的核心目标是缩短单个计算密集型任务的完成时间比如用多个线程同时渲染一帧画面的不同部分。C的std::thread和std::async是帮助我们实现并发和并行编程的工具。它们创建的线程最终由操作系统调度到CPU核心上执行。可能是并发交替也可能是并行同时这取决于你的硬件和操作系统调度策略。另一个必须理解的概念是线程安全。当多个线程访问同一块内存数据共享数据且至少有一个线程在修改它时如果没有任何保护措施程序的行为将是未定义的。数据竞争、死锁、活锁这些问题会接踵而至。因此我们在享受多线程带来的性能红利时必须时刻绷紧“线程安全”这根弦。C标准库提供了std::mutex互斥锁、std::atomic原子操作等工具来保卫我们的数据这部分我们会在后面详细展开。3. 手动挡的乐趣深入std::threadstd::thread是C多线程的基石。它的使用非常直接构造一个std::thread对象传入一个可调用对象函数、Lambda表达式、函数对象等一个新的线程便立即开始执行。3.1 基础创建与生命周期管理让我们从一个最简单的例子开始#include iostream #include thread void helloFunction() { std::cout Hello from thread! Thread ID: std::this_thread::get_id() std::endl; } int main() { std::cout Hello from main! Thread ID: std::this_thread::get_id() std::endl; // 创建线程并立即开始执行 helloFunction std::thread t(helloFunction); // 等待线程t执行完毕 t.join(); std::cout Main thread finished. std::endl; return 0; }这段代码有几个关键点构造即启动std::thread t(helloFunction);这一行执行后新线程t和主线程main就同时运行了。你无法控制它们谁先打印这是操作系统调度决定的。必须管理生命周期std::thread对象在其析构函数中会检查当前线程是否“可连接”。一个“可连接”的线程意味着它已经启动但尚未被join()或detach()。如果析构一个“可连接”的线程程序会直接调用std::terminate()终止这是一个非常容易踩的坑。join()阻塞当前线程这里是主线程直到t线程执行完毕。这确保了t线程的资源被正确回收。调用join()后t变为“不可连接”状态。detach()将t线程从std::thread对象中分离允许它独立地在后台运行。分离后t也变为“不可连接”状态你将失去对这个线程的直接控制权它的资源在线程结束后由操作系统自动回收。慎用detach()除非你非常清楚该线程的生命周期并且不关心它的执行结果。实操心得我个人的习惯是除非在设计守护线程或全局工作线程池等特定场景否则一律使用join()。使用RAII资源获取即初始化技术来管理std::thread的生命周期是更安全的方式。例如写一个ThreadGuard类在析构函数中判断并调用join()可以避免因异常抛出导致线程未被连接而程序崩溃的问题。3.2 参数传递与数据共享线程函数可以接受参数传递方式与std::bind类似默认情况下是值传递。这意味着参数会被拷贝到新线程的上下文中。void printValue(int x, const std::string str) { std::cout x x , str str std::endl; } int main() { int val 42; std::string msg Hello; // 参数 val 和 msg 会被拷贝到新线程中 std::thread t(printValue, val, msg); t.join(); // 此处修改 val 和 msg不会影响线程t中已经拷贝的值 val 100; std::cout Main val: val std::endl; return 0; }如果你想传递引用必须使用std::ref或std::cref进行包装。void modifyValue(int x) { x * 2; } int main() { int val 42; // 错误默认是值传递编译可能通过但修改的是副本原值不变。 // std::thread t(modifyValue, val); // 正确使用 std::ref 包装传递引用。 std::thread t(modifyValue, std::ref(val)); t.join(); std::cout Val after thread: val std::endl; // 输出 84 return 0; }数据共享与竞态条件当多个线程访问同一份数据时问题就来了。#include thread #include iostream #include vector int shared_counter 0; void increment() { for (int i 0; i 100000; i) { // 这是一个“读-改-写”操作非原子存在数据竞争 shared_counter; } } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout Final counter value: shared_counter std::endl; // 你很可能得不到 200000而是一个小于它的随机数。 return 0; }shared_counter这行代码在汇编层面可能对应“读取内存到寄存器”、“寄存器加一”、“写回内存”三个步骤。两个线程可能同时读取到同一个值比如100各自加一后都变成101再写回内存结果本应增加2却只增加了1。这就是典型的数据竞争。3.3 使用互斥锁std::mutex保护共享数据为了解决数据竞争我们需要引入锁机制。std::mutex是最基本的互斥锁。#include thread #include iostream #include vector #include mutex int shared_counter 0; std::mutex counter_mutex; // 定义一个互斥锁 void safe_increment() { for (int i 0; i 100000; i) { counter_mutex.lock(); // 加锁 shared_counter; // 临界区代码 counter_mutex.unlock(); // 解锁 } } int main() { std::thread t1(safe_increment); std::thread t2(safe_increment); t1.join(); t2.join(); std::cout Final counter value: shared_counter std::endl; // 稳定输出 200000 return 0; }现在当一个线程持有counter_mutex锁时另一个线程执行到lock()就会被阻塞直到锁被释放。这保证了shared_counter这个操作是原子的。注意事项直接使用lock()和unlock()非常危险。如果在加锁和解锁之间发生异常或者程序员忘记调用unlock()就会导致锁永远无法释放其他线程无限等待即死锁。因此永远不要直接调用lock()和unlock()3.4 使用 RAII 守卫std::lock_guard和std::unique_lockC标准库提供了RAII风格的锁管理类它们在构造时加锁析构时自动解锁异常安全。std::lock_guard简单易用构造即锁析构即解锁没有手动控制的接口。void safer_increment() { for (int i 0; i 100000; i) { std::lock_guardstd::mutex lock(counter_mutex); // 构造时自动 lock() shared_counter; // lock 析构时自动 unlock() } }std::unique_lock功能更强大但开销稍大。它允许延迟加锁、手动加解锁、转移所有权等。void flexible_increment() { std::unique_lockstd::mutex lock(counter_mutex, std::defer_lock); // 延迟加锁 for (int i 0; i 100000; i) { lock.lock(); // 手动加锁 shared_counter; lock.unlock(); // 手动解锁可以做一些非临界区的操作 // ... 其他非共享数据操作 ... } }实操心得对于99%的简单临界区保护std::lock_guard是首选它的零额外开销和极简语义减少了犯错的可能。只有当你需要std::condition_variable配合它需要std::unique_lock或者需要复杂的锁策略如转移锁所有权时才使用std::unique_lock。4. 自动挡的便捷探索std::async如果说std::thread是给你木材和工具让你自己造车那么std::async就是叫了一辆网约车。你告诉它目的地任务它帮你安排车辆线程和路线执行策略最后把你送到返回结果。4.1 基本用法与异步结果获取std::async的核心是它返回一个std::future对象。这个future是一个“期物”它代表一个未来才会得到的值。你可以通过future.get()来获取这个值这个调用会阻塞当前线程直到异步任务完成并返回值。#include iostream #include future #include chrono int computeHeavyTask(int x) { std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时计算 return x * x; } int main() { std::cout Main thread starting async task... std::endl; // 启动异步任务立即返回一个 future std::futureint fut std::async(std::launch::async, computeHeavyTask, 10); std::cout Main thread can do other work here... std::endl; // 主线程可以继续做其他事情 // 当需要结果时调用 get()这会阻塞直到任务完成 int result fut.get(); // 这里可能会等待2秒 std::cout Result from async task: result std::endl; // 输出 100 return 0; }4.2 启动策略std::launch::asyncvsstd::launch::deferredstd::async的第一个参数是启动策略它决定了任务如何执行std::launch::async强制要求在新创建的线程中异步执行任务。std::launch::deferred延迟执行。任务不会立即启动只有在调用future.get()或future.wait()时才会在调用者的线程中同步执行。std::launch::async | std::launch::deferred默认策略由标准库的实现决定。它可能在异步线程执行也可能延迟执行。这是不指定策略时的默认行为但也是最大的不确定性来源。// 示例deferred 策略 auto fut_deferred std::async(std::launch::deferred, [](){ std::cout Deferred task running in thread: std::this_thread::get_id() std::endl; return 42; }); std::cout Before get() std::endl; // 此时任务还未执行 auto result fut_deferred.get(); // 在这里任务才在当前线程主线程中执行 std::cout After get(), result: result std::endl;重要警告永远不要使用默认启动策略这是一个血泪教训。默认策略的“由实现定义”行为会导致程序的行为不可预测且严重依赖于编译器和标准库。例如任务的线程局部变量thread_local可能不会如你预期的那样初始化或者任务的并行性完全丧失。我的硬性规则是总是显式指定std::launch::async或std::launch::deferred。需要真正并发时用前者需要惰性求值时用后者。4.3std::async的线程管理与资源回收std::async的一个巨大优势是它简化了线程生命周期管理。你不需要手动join()或detach()。当std::future对象析构时如果异步任务是以async策略启动且尚未完成它的析构函数会阻塞等待任务完成。这听起来可能有点意外但它保证了异步操作的完成避免了资源泄漏。当然如果你不想在析构时阻塞可以将future保存起来或者使用shared_future。void fireAndForget() { // 错误临时 future 析构时会阻塞等待任务完成。 // 这实际上变成了“fire and wait”失去了异步的意义。 std::async(std::launch::async, [](){ std::this_thread::sleep_for(std::chrono::seconds(5)); std::cout Task done.\n; }); // 函数结束临时future析构主线程在这里等待5秒 std::cout Function ends? Not until task finishes!\n; } void betterFireAndForget() { // 正确做法将 future 存储到非局部作用域或者使用其他机制如线程池。 // 但 std::async 本身设计并非用于“发射后不管”。 auto fut std::async(std::launch::async, [](){ std::this_thread::sleep_for(std::chrono::seconds(5)); std::cout Task done.\n; }); // fut 被移出只要 fut 不被析构主线程就不会阻塞。 // 通常需要将 fut 存入一个全局容器或交给专门的管理器。 }因此std::async更适合于需要获取结果的、有明确生命周期的异步任务而不是后台守护任务。5.std::thread与std::async的深度对比与选型指南了解了各自的基本用法后我们来一场面对面的较量。选择哪一个从来不是谁好谁坏的问题而是场景匹配的问题。特性维度std::threadstd::async(withstd::launch::async)控制粒度精细控制。直接管理线程对象可以join,detach, 获取id, 转移所有权。粗粒度控制。隐藏了线程管理细节通过future与任务交互。生命周期管理手动管理。必须显式join或detach否则析构时程序终止。自动管理。future析构会等待任务完成防止泄漏。异常传播困难。线程函数中抛出的异常如果不捕获会调用std::terminate。内置支持。异步任务中抛出的异常会在调用future.get()时重新抛出可以在主线程捕获。返回值获取不便。需要通过共享变量、Promise/Future模式如std::promise/std::future或回调函数来传递结果。直接支持。直接返回std::futureT通过get()获取结果或异常。线程资源直接创建。每次构造都很可能创建一个新的操作系统线程。可能复用。标准库可能使用内部线程池来执行任务但C标准不保证主流实现如GCC/Clang的libstdc/libc通常不复用MSVC可能复用。适用场景1. 需要长时间运行的后台线程如网络监听、游戏循环。2. 需要精细控制线程优先级、亲和性等需平台相关API。3. 构建复杂的线程间通信和同步模式。1. 简单的“计算-返回”型异步任务。2. 需要方便地获取异步结果和异常。3. 希望简化代码避免手动线程管理。选型决策树你的任务需要长时间运行还是短期计算长期运行如服务、监听器选std::thread。用detach()或精心管理的join()。短期计算需要结果优先考虑std::async。你需要对线程有绝对控制权吗是如设置CPU亲和性、实时优先级必须用std::thread 平台API。否std::async更省心。你担心异常处理吗是希望异常能自然传播回主线程std::async是更好的选择。否或自己能在线程内妥善处理两者皆可。你是在编写一个库还是应用程序库谨慎使用std::async因为其默认策略和线程资源行为由实现定义可能给库用户带来意外。std::thread的行为更确定。应用程序根据上述条件选择。我的经验法则对于应用程序中大多数“发起一个计算任务然后等待结果”的场景首选std::async。它的异常安全和自动资源管理能避免大量低级错误。只有当std::async无法满足需求如需要线程池、复杂同步、长期运行时才动用std::thread这把更锋利、也更危险的“手术刀”。6. 高级话题与性能陷阱6.1 线程池与std::async的局限性虽然std::async可能复用线程取决于实现但它不是一个真正的线程池。频繁地使用std::async启动大量微小任务可能导致操作系统线程的频繁创建和销毁带来巨大的开销。对于高并发、短生命周期的任务队列自己实现一个线程池或使用第三方库如 Intel TBB Microsoft PPL通常是更好的选择。一个简单线程池的核心思想是预先创建一组工作线程std::thread它们从一个线程安全的队列中不断取出任务可调用对象并执行。主线程只需将任务提交到队列即可。6.2std::future的局限性与std::shared_futurestd::future对象是移动仅有的其结果只能被获取get()一次。如果你有多个消费者需要同一个异步结果就需要std::shared_future。它可以被复制每个副本都可以独立地调用get()。auto fut std::async(std::launch::async, [](){ return 42; }); // std::futureint fut2 fut; // 错误future不可复制。 std::shared_futureint shfut fut.share(); // 将 future 转换为 shared_future // 现在可以复制 shared_future auto shfut_copy1 shfut; auto shfut_copy2 shfut; std::thread t1([shfut_copy1](){ std::cout shfut_copy1.get() std::endl; }); std::thread t2([shfut_copy2](){ std::cout shfut_copy2.get() std::endl; }); t1.join(); t2.join(); // 两个线程都能正确获取到值 426.3 死锁的识别与预防死锁是并发编程的噩梦。典型场景是线程A锁定了互斥量M1试图锁定M2同时线程B锁定了M2试图锁定M1。双方都在等待对方释放资源程序永久挂起。预防死锁的黄金法则避免嵌套锁如果必须锁多个互斥量使用std::lock或std::scoped_lock(C17) 来一次性锁定多个互斥量避免因加锁顺序不一致导致的死锁。std::mutex m1, m2; // 危险的做法可能死锁 // thread1: { lock(m1); lock(m2); } // thread2: { lock(m2); lock(m1); } // 安全的做法 void safe_operation() { std::scoped_lock lock(m1, m2); // C17一次性锁定所有使用死锁避免算法 // 或者 C11/14: // std::lock(m1, m2); // 一次性锁定顺序不重要 // std::lock_guardstd::mutex lk1(m1, std::adopt_lock); // std::lock_guardstd::mutex lk2(m2, std::adopt_lock); // ... 操作共享数据 ... }固定锁顺序如果无法一次性锁定在所有线程中强制规定相同的加锁顺序。使用层次锁为锁定义逻辑层次只允许按层次顺序加锁例如必须先锁高层次的锁才能锁低层次的锁。避免在持有锁时调用用户代码因为你不知道用户代码会不会再去获取别的锁。6.4 性能考量锁粒度、争用与无锁编程锁是性能杀手。过度使用或粗粒度的锁会导致线程频繁阻塞使并发程序退化为串行。细化锁粒度用多个细粒度的锁保护不同的数据而不是一个大锁保护所有数据。减少每个锁持有的时间。识别锁争用使用性能分析工具如 perf, VTune查看线程在锁上的等待时间。高争用是性能瓶颈的标志。考虑无锁数据结构对于简单的计数器、标志位使用std::atomic类型。std::atomicint的fetch_add操作是原子的且通常比“互斥锁普通int”快得多。std::atomicint atomic_counter{0}; void fast_increment() { for (int i 0; i 100000; i) { atomic_counter.fetch_add(1, std::memory_order_relaxed); // 无锁操作 } }std::atomic和内存序memory_order本身是一个深水区对于大多数应用使用默认的memory_order_seq_cst顺序一致性是安全且简单的。只有在极端性能敏感的场景才需要研究更宽松的内存序。7. 实战案例一个简单的并行计算示例让我们用一个实际例子来整合所学知识计算一个大型向量中所有元素的平方和。我们将对比单线程、手动std::thread分块、以及std::async分块三种实现。#include iostream #include vector #include numeric #include chrono #include thread #include future #include cassert // 1. 单线程基线版本 long long squareSumSequential(const std::vectorint data) { long long sum 0; for (int val : data) { sum static_castlong long(val) * val; } return sum; } // 2. 使用 std::thread 手动分块并行版本 long long squareSumThreads(const std::vectorint data, int num_threads) { std::vectorstd::thread workers; std::vectorlong long partial_sums(num_threads, 0); size_t chunk_size data.size() / num_threads; auto worker_func [](int thread_id) { size_t start thread_id * chunk_size; size_t end (thread_id num_threads - 1) ? data.size() : start chunk_size; long long local_sum 0; for (size_t i start; i end; i) { local_sum static_castlong long(data[i]) * data[i]; } partial_sums[thread_id] local_sum; }; for (int i 0; i num_threads; i) { workers.emplace_back(worker_func, i); } for (auto t : workers) { t.join(); } // 规约部分和 return std::accumulate(partial_sums.begin(), partial_sums.end(), 0LL); } // 3. 使用 std::async 分块并行版本 long long squareSumAsync(const std::vectorint data, int num_tasks) { std::vectorstd::futurelong long futures; size_t chunk_size data.size() / num_tasks; for (int i 0; i num_tasks; i) { size_t start i * chunk_size; size_t end (i num_tasks - 1) ? data.size() : start chunk_size; // 使用 async 启动异步任务计算子块 futures.push_back( std::async(std::launch::async, [data, start, end]() - long long { long long local_sum 0; for (size_t j start; j end; j) { local_sum static_castlong long(data[j]) * data[j]; } return local_sum; }) ); } // 收集结果 long long total_sum 0; for (auto fut : futures) { total_sum fut.get(); // get() 会等待每个任务完成 } return total_sum; } int main() { const size_t data_size 10000000; std::vectorint data(data_size); std::iota(data.begin(), data.end(), 1); // 填充 1, 2, 3, ... data_size int num_workers std::thread::hardware_concurrency(); // 获取硬件支持的并发线程数 if (num_workers 0) num_workers 4; std::cout Using num_workers threads/tasks.\n; auto start std::chrono::high_resolution_clock::now(); auto sum_seq squareSumSequential(data); auto end std::chrono::high_resolution_clock::now(); std::chrono::durationdouble elapsed_seq end - start; std::cout Sequential time: elapsed_seq.count() s, sum: sum_seq std::endl; start std::chrono::high_resolution_clock::now(); auto sum_threads squareSumThreads(data, num_workers); end std::chrono::high_resolution_clock::now(); std::chrono::durationdouble elapsed_threads end - start; std::cout std::thread time: elapsed_threads.count() s, sum: sum_threads std::endl; start std::chrono::high_resolution_clock::now(); auto sum_async squareSumAsync(data, num_workers); end std::chrono::high_resolution_clock::now(); std::chrono::durationdouble elapsed_async end - start; std::cout std::async time: elapsed_async.count() s, sum: sum_async std::endl; assert(sum_seq sum_threads sum_seq sum_async); return 0; }运行结果分析 在我的8核机器上对于一千万的数据量std::thread和std::async版本通常能达到单线程版本的4-6倍加速。std::async版本的代码明显更简洁我们不需要手动管理线程向量和join()。但请注意这里我们为每个子任务都显式指定了std::launch::async。性能提示并行计算并非总是更快。线程的创建、销毁、同步如本例中最后的join和get都有开销。只有当每个任务的计算量足够大足以掩盖这些开销时并行化才有收益。对于非常小的任务串行可能更快。这就是著名的“阿姆达尔定律”和“开销模型”需要考虑的。8. 常见问题与排查技巧实录即使理解了原理实际编码中依然会碰到各种妖魔鬼怪。下面是我总结的一些典型问题和排查思路。8.1 程序崩溃terminate called without an active exception问题描述程序运行时突然崩溃提示上述信息。根本原因一个std::thread对象在析构时其关联的线程仍然是“可连接”的即既没有join()也没有detach()。排查步骤检查所有std::thread对象的生命周期。确保在它们离开作用域或所在对象析构前已经调用了join()或detach()。特别注意代码路径中可能提前返回或抛出异常的地方。在这些地方线程可能未被正确清理。使用RAII包装器是终极解决方案。class ThreadJoiner { std::thread t; public: explicit ThreadJoiner(std::thread t_) : t(t_) {} ~ThreadJoiner() { if (t.joinable()) t.join(); } // 禁止拷贝 ThreadJoiner(const ThreadJoiner) delete; ThreadJoiner operator(const ThreadJoiner) delete; }; void riskyFunction() { std::thread t([](){ /* long task */ }); ThreadJoiner j(t); // 守卫对象 // ... 可能抛出异常的代码 ... // 即使异常抛出j的析构函数也会确保t被join // 不需要手动调用 t.join(); }8.2 数据不一致或结果随机错误问题描述多线程程序运行结果每次都不一样或者最终结果与预期不符。根本原因数据竞争。多个线程未加锁访问了共享的可变数据。排查步骤审查所有共享变量找出所有被多个线程读写的全局变量、静态变量、引用或指针传递的参数。为每个共享变量确定保护机制对于简单的标志或计数器考虑std::atomic对于复杂数据结构使用std::mutex。使用工具辅助Clang/LLVM 和 GCC 都提供-fsanitizethread编译选项ThreadSanitizer可以在运行时检测数据竞争。这是排查此类问题的神器。缩小临界区确保锁只保护必要的最小代码段。在持有锁时不要进行IO操作或调用可能很慢的外部函数。8.3 程序运行速度没有提升甚至更慢问题描述使用了多线程但程序执行时间没有减少或者反而增加了。可能原因与排查任务粒度过小线程创建和同步的开销超过了并行计算带来的收益。尝试增大每个线程处理的数据块。锁争用严重多个线程频繁竞争同一把锁导致大部分时间在等待。使用性能分析工具查看锁的等待时间。考虑使用更细粒度的锁或无锁数据结构。虚假共享多个线程频繁修改位于同一缓存行Cache Line通常64字节的不同变量。这会导致CPU缓存频繁失效性能急剧下降。解决方法是让可能被不同线程频繁修改的变量在内存中保持足够的距离对齐到缓存行大小。struct alignas(64) PaddedCounter { // C11 后使用 alignas long long value; // 假设这是被一个线程频繁修改的 // char padding[64 - sizeof(long long)]; // 老式手动填充方法 }; std::vectorPaddedCounter counters(num_threads);CPU核心数未充分利用确保创建的线程数量与硬件并发线程数std::thread::hardware_concurrency()相匹配。创建过多线程会导致过多的上下文切换开销。8.4std::async任务没有并行执行问题描述使用了std::async但程序行为像是串行的没有观察到CPU使用率上升。根本原因使用了默认启动策略或std::launch::deferred策略。解决方案始终显式指定启动策略。如果你期望并行使用std::async(std::launch::async, ...)。同时确保std::future对象没有被立即析构例如忽略返回值否则会变成同步等待。多线程并发编程是C从“系统语言”迈向“现代应用语言”的关键一步。std::thread给了你底层控制力而std::async提供了高层抽象和安全性。没有银弹只有最适合场景的工具。从我个人的经验来看在项目初期多用std::async可以快速构建原型并减少错误当性能瓶颈出现需要精细调优时再考虑用std::thread配合锁、条件变量乃至无锁数据结构进行深度优化。记住并发编程的第一要义是正确性第二才是性能。在写出正确的、无数据竞争的代码之前不要过早追求极致的并行效率。