多线程与顺序执行:从C++到Python的并发控制实战解析

发布时间:2026/9/9 19:51:58
多线程与顺序执行:从C++到Python的并发控制实战解析 多线程和顺序执行这两个词放在一起怎么看都像是个悖论。当初我做第一个并发任务的时候也被这个需求问懵过既然都用多线程了为什么还要让它们顺序执行那直接用单线程不就行了后来在真正的项目里踩过几次坑才明白这个需求的背后往往藏着更复杂的现实——有些任务必须并发地去准备数据但最终落盘或者提交的时候又必须严格按顺序来。这是并发编程里一个非常经典的话题C、Python、Java 的生态里都有对应的解法而且解法思路还不太一样。这篇文章不打算只讲某一个语言我尽量把“多线程实现顺序执行”这件事给你拆透了。从最朴素的 join 思路到条件变量、信号量、任务队列、状态机再到 Spring Boot 和 MySQL 那些和“顺序执行”相关的常见误区全部过一遍。你会发现“顺序执行”这四个字在不同场景下指向的是完全不同的东西。1. 先看需求并发体系里顺序执行到底是什么先说个有意思的现象。不少人第一次听到“多线程实现顺序执行”第一反应都是这需求是不是写反了其实没有。真实业务里这类需求非常常见而且远比你想象的要普遍。我就遇到过好几个典型的场景。1.1 真实场景哪些业务需要并发准备顺序落地最典型的一类是日志落盘。我在做网关中间件的时候每个请求进来都会产生好几十条访问日志每秒几百个请求日志的生成天然是并发的。但落到磁盘文件的时候日志条目必须保持时间先后顺序否则排查问题的时候看到的日志全是乱的根本没法用。这就是“并发生产、顺序消费”的典型形态。另一类是批量任务中的依赖关系。比如一个数据同步任务需要先从 A 接口拉数据再经过 B 服务的清洗最后写入 C 存储。这个流水线里的每一步内部的子任务可以拆成多线程并行处理但整条流水线的步骤之间必须严格按 A→B→C 的顺序推进。你要是让 B 步骤在 A 步骤还没完成的时候就开始跑拿到的就是残缺数据。还有一类是多线程并发计算结果的有序汇总。比如你有 10 个线程各自处理一个分片处理完了之后要把结果拼起来。如果分片 1 的结果晚于分片 2 才就绪但最后输出的文件里必须分片 1 在前这就需要一种“谁先完成不重要但汇总时必须按编号顺序来”的机制。这类场景在很多分布式计算框架里都有其实原理就是“乱序完成按序提交”。1.2 线程的顺序到底指什么很多工具文档里讲顺序执行但从来不解释“顺序”这个词的三个不同层次导致很多人理解偏了。我自己把它们拆成三层第一层线程启动的顺序。就是 main 线程里按照代码顺序调用thread1.start()、thread2.start()。这一步只是把线程交给操作系统调度器它俩之后谁先跑、谁后跑完全由调度器决定你控制不了也不该控制。第二层线程完成的顺序。就是哪个线程先执行完、退出。这个顺序和启动顺序没有任何必然关系。你让 A 先启动不代表 A 先执行完。系统调度、CPU 时间片分配、资源竞争都会影响完成顺序。第三层任务的语义顺序。就是任务之间业务上的先后依赖关系。比如“先把订单写入数据库再发送通知短信”这两个动作谁先谁后是业务逻辑规定的如果反了就是事故。绝大多数时候我们要实现的是第三层的语义顺序只不过实现手段恰好要借助线程同步机制。想明白自己要的是哪一层再去看技术和方案思路会清晰很多。1.3 什么场景下顺序执行是不必要的这一点没想清楚的话容易过度设计。我见过有同事为了“保证数据顺序”给所有线程加了一把大锁结果并发性能直接掉成单线程还美其名曰“确保一致性”。这就是没区分好“必须有序”和“天然无序”的边界。判断标准其实就一条如果两个操作最终修改的是同一个资源并且结果和操作顺序有关那么这个顺序就是有意义的必须保证如果两个操作互不干扰或者最终结果和顺序无关就别强行加同步。比如多个线程各自往自己的数组里写数据最后汇总时再统一排序这个过程就没必要全程加锁只需要在汇总那一步按索引拼接就行。这类优化在很多高性能计算库里有大量应用理解了边界才能既保证正确性又保住性能。2. C 下控制线程执行顺序从 join 到条件变量C 的多线程生态我用了很多年从最初的std::thread到后面的std::async、信号量实践体验下来控制顺序的手段是层层递进的。这一节按复杂度从低到高给你捋一遍。2.1 join 的局限性它只解决主线程等待不解决子线程互等先说最朴素的方式std::thread::join()。它的作用是让当前线程阻塞直到目标线程执行完毕。如果你有三个任务需要依次完成最直白的写法就是std::thread t1(task1); t1.join(); std::thread t2(task2); t2.join(); std::thread t3(task3); t3.join();这种写法实现顺序吗实现了。有并发吗没有。因为t1.join()之后的代码要等t1完全结束才会执行所以在t1运行期间t2根本还没创建更别说并行。这实际上就是串行执行套了一层线程的外壳性能上没有任何收益。那join到底该怎么用在我的实践里它适合用在主线程收尾的场景。比如你创建了 4 个线程并行处理 4 个分片数据每个线程各干各的互不依赖最后所有线程都跑完之后主线程再做汇总。这时候只需要在启动所有线程后对每个线程调用join()等待全部完成。std::vectorstd::thread workers; for (int i 0; i 4; i) { workers.emplace_back([i] { processSlice(i); }); } for (auto t : workers) { t.join(); // 等待所有线程完成 } // 这里所有分片都处理完了可以做汇总这个场景里join的语义是“屏障”不是“顺序”。它保证的是并发任务全部结束这个时间点而不是任务之间的先后关系。很多人一听说用join实现顺序执行就直接套用结果把好好的多线程程序硬生生写成了串行这是最典型的误用。2.2 mutex condition_variable线程间相互等待的经典解法如果你需要的是线程 A、B、C 不并行而是严格按 A→B→C 的顺序依次执行join就无能为力了。这时候要用条件变量。这是我个人最推荐的方式之一因为它逻辑清晰、性能可控而且几乎每个多线程框架都有对应的原语。先看需求线程 1 打印 A线程 2 打印 B线程 3 打印 C要求输出永远是 ABC不管你启动线程的顺序怎么变。#include iostream #include thread #include mutex #include condition_variable std::mutex mtx; std::condition_variable cv; int turn 1; // 当前轮到哪个线程执行 void printChar(int id, char ch) { std::unique_lockstd::mutex lock(mtx); while (turn ! id) { // 注意这里是 while不是 if cv.wait(lock); // 还没轮到挂起等待 } std::cout ch std::endl; turn; cv.notify_all(); // 唤醒所有在等待的线程 } int main() { std::thread t1(printChar, 1, A); std::thread t2(printChar, 2, B); std::thread t3(printChar, 3, C); t1.join(); t2.join(); t3.join(); return 0; }这段代码的核心是一个共享变量turn它像一个令牌只有轮到谁谁才能执行。线程 1 启动后判断turn 1成立输出 A然后turn变成 2调用notify_all()通知其他线程。线程 2 和线程 3 正在wait()里睡觉被唤醒后重新判断turn ! id是否成立只有线程 2 发现turn 2才真正往下走。这里面有三个易错点每个我都踩过第一判断条件要用while而不是if。虽然这里用if在绝大多数时候也能跑对但条件变量存在“伪唤醒”的问题也就是线程可能在条件不满足的时候被唤醒。用if的话唤醒后不会重新检查条件直接往下走就出错了。用while能保证唤醒后重新判断条件这是教科书级别的容错处理。第二持有锁的时间要尽可能短。上面代码里从判断条件到输出到修改turn再到notify_all整个过程都在锁保护下这是为了确保turn的修改和判断是一个原子操作。但如果在printChar里混入大量耗时的计算就会阻塞其他线程的锁获取并发效率大减。好的做法是同步部分只做状态判断和更新真正耗时的业务逻辑放到锁外面void printChar(int id, char ch) { { std::unique_lockstd::mutex lock(mtx); while (turn ! id) { cv.wait(lock); } turn; // 内部先交出令牌 } // 锁在这里释放 cv.notify_all(); // 通知可以放在锁外 std::cout ch std::endl; // 业务输出放在锁外 }第三notify_all和notify_one怎么选。只有一个线程在等待下一轮的时候用notify_one就够了开销更小。但如果你不确定有几个线程在等待或者等待关系比较复杂用notify_all更安全。这个例子中三个线程都可能同时在等所以我直接用notify_all简单可靠。2.3 信号量和 promise/future更现代的替代方案条件变量虽然通用但手写turn状态管理的代码量不小而且逻辑一旦复杂比如控制 10 个步骤就很容易出错。C20 之后有了std::counting_semaphore实现同样需求的代码会简洁不少。#include semaphore std::counting_semaphore3 sem(0); void taskA() { // 执行任务 A sem.release(); // 发放一个许可 } void taskB() { sem.acquire(); // 等待 A 的许可 // 执行任务 B }信号量的思路可以理解成“通行证”A 完成之后发放一张通行证B 必须拿到这张通行证才能继续。这样 A 和 B 就形成了先后关系。不过要提醒一句C20 的信号量需要编译器支持较新的标准如果还在用 C14/17条件变量还是最稳妥的选择。另一个很有用的机制是std::promise和std::future。它解决的问题是跨线程传递结果并且天然带有“等待”语义。比如主线程发起了一个异步任务这个任务什么时候完成不确定但主线程需要拿到结果才能继续std::promiseint resultPromise; std::futureint resultFuture resultPromise.get_future(); std::thread worker([resultPromise] { // 模拟耗时计算 std::this_thread::sleep_for(std::chrono::seconds(1)); resultPromise.set_value(42); // 计算完成设置结果 }); // 主线程做其他事情... // 需要结果了阻塞等待 int value resultFuture.get(); std::cout result: value std::endl; worker.join();future.get()会阻塞当前线程直到对应的promise设置了值。这本质上就是一种“顺序保证”拿结果的动作一定发生在任务完成之后。相比手动维护标志位加自旋等待promise/future的写法更安全也不会浪费 CPU。我在实际项目里用它做过多线程计算结果的有序汇聚体验很好。3. Python 线程想要顺序执行先看懂 GIL 再说Python 的情况和 C 完全不一样因为 GIL全局解释器锁的存在它的多线程在 CPU 密集型任务上基本是“假的并行”。但控制顺序这件事Python 反而有更顺手的工具。3.1 GIL 到底限制了什么不限制什么先把这个概念说透。GIL 是 CPython 解释器里的一把全局锁它保证同一时刻只有一个线程在执行 Python 字节码。所以你跑threading模块开十几个线程做纯计算本质上还是轮流占用单个 CPU 核心性能不会因为核心数增加而提升。但是这不代表 Python 多线程没用。在I/O 密集型任务网络请求、文件读写、数据库访问里线程等待 I/O 时会让出 GIL其他线程就能拿过来继续跑。所以 Python 多线程在网络并发场景下的效果是非常明显的。那 GIL 对“顺序执行”有什么影响其实影响很小因为顺序控制是逻辑层面的约束和底层是否并行没关系。不管是真并行还是假并行该用锁和队列的地方一个都不能少。3.2 顺序执行的首选方案Queue 天然的顺序消费如果目标是把多个线程生成的任务按一定的顺序执行Python 里最简单可靠的方法是用queue.Queue。它本身就是线程安全的内部实现了锁机制多个生产者往里放数据一个消费者按入队顺序取出执行天然有序。我在做爬虫项目的时候用过这个模式爬虫分成多个线程去抓取网页抓回来之后需要按 URL 的编号顺序存储到文件里。如果是“谁先抓完谁先存”最终文件的顺序就是乱的。我的做法是给每个 URL 编一个序号把抓取结果放进一个优先队列主消费线程按序号依次取出写入文件。import threading import queue import time result_queue queue.Queue() WORKERS 5 def fetch_data(worker_id): 模拟多个线程并发抓取数据 for i in range(worker_id, 20, WORKERS): time.sleep(0.1) # 模拟 I/O 等待 result_queue.put((i, fdata-{i}-from-worker-{worker_id})) result_queue.put((None, DONE)) threads [] for w in range(WORKERS): t threading.Thread(targetfetch_data, args(w,)) threads.append(t) t.start() # 消费端按编号顺序取结果 received {} done_count 0 expected_next 0 while done_count WORKERS: idx, data result_queue.get() if idx is None: done_count 1 continue received[idx] data # 按顺序输出可以连续拼接的结果 while expected_next in received: print(fsave {received[expected_next]}) del received[expected_next] expected_next 1 for t in threads: t.join()这个例子核心思路不是让所有线程排队执行而是让它们并发去干活但结果提交的时候按顺序拼接。expected_next像一个指针永远指向下一个应该落盘的编号。编号 5 的结果早就到了没用必须等前面的 4 落地后才能拼接它。这种方式既保留了多线程的并发收益又保证了最终输出的顺序。3.3 用 Event 和 Lock 控制步骤之间的小步推进如果你要控制的不是“结果提交顺序”而是“线程之间的步骤推进顺序”用threading.Event会更直观。Event 就像一个标志位一个线程wait()等待标志被设置另一个线程完成任务后set()放行。import threading import time event_b threading.Event() event_c threading.Event() def task_a(): print(step A start) time.sleep(1) print(step A done) event_b.set() # 放行 B def task_b(): event_b.wait() # 等待 A 完成 print(step B start) time.sleep(1) print(step B done) event_c.set() # 放行 C def task_c(): event_c.wait() # 等待 B 完成 print(step C start) time.sleep(1) print(step C done) threading.Thread(targettask_a).start() threading.Thread(targettask_b).start() threading.Thread(targettask_c).start()Event适合任务数量少、依赖关系清晰的场景。任务一多事件满天飞代码就会乱。这种时候我建议用锁加状态值的方式逻辑更可控。Python 的threading.Lock加一个简单的轮次变量效果和上一节 C 的条件变量类似只不过 Python 的Condition对象封装得更方便import threading condition threading.Condition() turn 1 def print_char(thread_id, ch): global turn with condition: while turn ! thread_id: condition.wait() print(ch) turn 1 condition.notify_all() threads [ threading.Thread(targetprint_char, args(1, A)), threading.Thread(targetprint_char, args(2, B)), threading.Thread(targetprint_char, args(3, C)), ] for t in threads: t.start() for t in threads: t.join()这段代码的逻辑和 C 版几乎一模一样。多个线程共用一个Condition配合一个共享的turn变量谁持有轮次谁就干活干完把轮次交出去并通知所有人重新竞争。这里要注意的是 Python 的Condition.wait()会自动释放锁并挂起被唤醒后会重新获取锁和 C 的行为是一致的。3.4 如果要的是按放入顺序处理ThreadPoolExecutor 也有讲究concurrent.futures.ThreadPoolExecutor是 Python 里更高级的线程池封装大多数人用它的姿势是submit()然后future.result()。但这里有个隐蔽的顺序问题submit()的调用顺序和任务完成的顺序没有关系。比如我往线程池里连续提交了 10 个任务它们的执行结果可能完全乱序返回。如果只是各自打印那你看到的就是乱序输出如果后续逻辑依赖“第一个提交的任务处理完了才能处理第二个”你需要用executor.map()它保持结果的顺序和输入顺序一致from concurrent.futures import ThreadPoolExecutor def process(item): return fprocessed-{item} with ThreadPoolExecutor(max_workers4) as executor: # map 会保持结果顺序和输入顺序一致 results list(executor.map(process, range(10))) for r in results: print(r)map()内部会等待所有任务完成然后按输入顺序返回结果。它适合“任务可以并发执行但结果需要按序收集”的场景。如果你需要的是“任务必须严格一个跑完再跑下一个”那线程池就不合适了直接用单线程循环或队列消费更合理。4. 千万别把线程顺序和框架并发模型混为一谈热搜词里还有几个有意思的关联词——Spring Boot 请求是不是多线程的、MySQL 的执行顺序这些其实和多线程顺序执行有间接关系但经常被混在一起讨论导致很多人产生误解。这一节把它们拆清楚。4.1 Spring Boot 的请求是多线程吗是但和你理解的顺序不是一回事Spring Boot 应用默认内嵌 Tomcat而 Tomcat 的工作方式就是多线程的每一次 HTTP 请求进来容器会从线程池里分配一个线程去处理请求处理完线程归还给池子。所以答案是是Spring Boot 的请求处理本身就是多线程的。但这个“多线程”和“多核并行”是两码事你可以把它理解为“多个请求同时在被处理但每个请求内部代码的执行依然是单线程顺序的”。这给很多初学者带来一个误导以为在 Controller 里写的代码天然是“并发安全”的因为“一个请求一个线程”。不对一个请求一个线程只说明这个请求内部的执行是顺序的但多个线程并发操作同一个资源时该加锁还是要加锁该用原子类还是要用原子类。比如一个全局计数器两个请求同时进来执行counter这行代码看起来是一行实际上底层是读取、加一、写回三步并发下就会丢更新。如果你在 Spring Boot 里想控制多个请求之间的执行顺序那就更复杂了因为哪个请求先被 Tomcat 的线程池调度到完全不受你控制。这时候的“顺序执行”通常要靠业务层面保证比如把请求按某种 key 分到同一个队列用单线程消费或者引入消息中间件来做有序消息。有个原则要记住别指望 Web 容器帮你排队它只管并发和吞吐。4.2 MySQL 的执行顺序是另一码事SQL 语义顺序不是线程顺序热搜词里的 “mysql执行顺序” 其实指的是 SQL 的逻辑执行顺序。比如一条多表联查加分组排序的 SQLSELECT department, COUNT(*) FROM employees WHERE status active GROUP BY department HAVING COUNT(*) 10 ORDER BY department;这玩意儿真正的执行逻辑是先从 FROM 读取表经 WHERE 过滤再 GROUP BY 分组之后 HAVING 过滤分组接着 SELECT 计算最后 ORDER BY 排序。这个顺序是 SQL 解析器规定的语义顺序和“多线程”没有半点关系。但 MySQL 底层确实有并发执行的部分InnoDB 存储引擎的多版本并发控制MVCC、行锁、间隙锁这些机制就是为了处理多个并发事务时“谁先写、谁读到什么”的问题。这里也有顺序的概念但它是由事务、锁和隔离级别决定的不是你能直接用线程 API 控制的。如果你想在并发写入 MySQL 时保证“数据按业务顺序提交”常见的做法是利用自增主键保证插入顺序或者在业务表里加一个序号字段在应用层按序号排序后再写入。数据库端的“顺序执行”大多数是靠约束和索引实现的而不是靠线程同步。理解这一点你就不会把多线程的顺序执行方案错误地往 SQL 执行上套。4.3 为什么分布式系统里全局顺序是个奢侈品说到 MySQL 和 Spring Boot自然会往分布式方向联想。有一个事实值得在这里说透在单机多线程环境下实现顺序执行相对简单因为所有线程共享同一块内存锁、队列、状态变量随手就有。但到了分布式系统多台机器各自跑着自己的线程想做到全局顺序成本立刻指数级上升。原因很朴素分布式环境下没有一个共享的turn变量也没有全局可见的锁。要协调多台机器上的任务顺序要么引入一台中心化的调度器比如 Zookeeper 的分布式锁要么把所有需要保序的消息发到同一个队列里用单消费者处理。这两种方案都有性能和可用性代价所以成熟的分布式系统通常会做“分区有序”——比如 Kafka 里同一个 partition 内的消息天然有序但不同 partition 之间不保证。用局部有序替代全局有序是工程上最常见的妥协方案。理解这一点对你判断技术选型非常重要。5. 三种通用设计模式根治跨线程顺序问题前面讲的都是具体语言的具体工具。但如果你在多个项目里反复遇到“多线程顺序执行”的问题我建议再往上抽象一层从设计模式的角度去思考。这几种模式我总结下来基本能覆盖 90% 以上的需求。5.1 模式一令牌环——轮流发言令牌环Token Ring本质上就是前面 C 例子里的turn变量思路。多个线程共用一个令牌只有拿到令牌的线程才有资格执行执行完把令牌传给下一个。这个模式适合任务数量固定、步骤可以枚举的场景比如三个线程按 1→2→3 循环打印。优点逻辑极其清晰代码容易维护不会出现两个线程同时推进的情况。缺点扩展性不好步骤一多状态变量和判断逻辑都会膨胀而且等到第 N 步的时候前面 N-1 个线程全部处于等待状态本质上又是串行。适用建议步骤少于 5 个或者只是为了演示/教学场景再用它。真实的业务系统里步骤一旦超过 5 个我强烈建议换成状态机框架或者队列方案。5.2 模式二任务队列 单消费者——一个口子进一个口子出这个模式是我在实际工程中最推崇的把任务丢进一个线程安全的队列然后用一个消费者线程逐个取出来执行。这样任务的生产可以是并发的但消费和执行严格串行天然满足顺序要求。这个模式的优点在于解耦生产者不关心消费的顺序消费者不关心是谁生产的。要扩容的时候多个消费者并行消费即可只要在需要保序的任务上按编号合并就行。Python 的queue.Queue、Java 的LinkedBlockingQueue、C 的concurrent_queue在 TBB 或微软的 PPL 中都有现成实现。我做过一个订单导出系统导出请求非常多几千个订单同时发起导出。如果让每个请求都独立起一个线程去写文件文件系统很快就撑不住了。我的方案是每个导出请求生成一个任务放进队列一个后台消费者线程逐条执行导出。文件的生成顺序和请求提交顺序一致资源占用也特别平稳。5.3 模式三并发计算 按序提交——各干各的汇总时再排序这个模式前面 Python 的例子已经演示过了多个线程并发执行任务每个任务带一个序号结果先存到一个临时容器里最后汇总时按序号拼接。它允许任务的执行过程完全并行只在“提交结果”这个步骤上做顺序保证。这个模式适合计算密集型和数据分片型的任务因为它的重点不在于让线程排队而是要榨干多核 CPU 的并发能力最后输出的时候再恢复顺序。像归并排序的并行版本、MapReduce 的 reduce 阶段本质都是这个思路。实现的时候有几个细节要注意。结果容器必须线程安全可以用带锁的 map 或字典序号管理要提前分配好不能在执行过程中动态生成否则无法确定“下一个序号”是谁。代码层面需要一个类似expected_next的游标不断检查“下一个该提交的编号是否已经到了”到了就提交然后游标加一。5.4 模式选型对照表这几个模式乍看容易混我整理了一张对照表方便你按需求场景快速选择需求特征推荐模式核心优势坑点提示固定步骤、数量少令牌环 / 状态机逻辑简单直观易调试步骤多了变成串行状态管理复杂任务动态产生、需要保序执行队列 单消费者解耦性强天然有序单消费者可能成性能瓶颈计算可并发、结果必须有序并发计算 按序提交并发收益最大化需要额外处理结果容器的线程安全跨进程、跨机器的全局顺序分布式锁/消息队列分区能解决分布式保序成本高、有可用性风险6. 实战中最容易翻车的几个线程顺序坑这一节写点我在实际项目里踩过的坑每个都是真金白银换来的教训。你照着这些点自查可以少走很多弯路。6.1 死锁一个隐蔽的“互相等待”陷阱条件变量和锁用得多了最怕的就是死锁。经典的死锁场景是两个线程各持有一把锁又都在等对方释放另一把锁。比如线程 1 拿着锁 A 等锁 B线程 2 拿着锁 B 等锁 A两边都等不到程序就挂死了。多线程顺序执行这个需求里死锁也经常出现。一个常见场景是你在条件变量里wait()之前忘了把锁释放掉或者嵌套获取锁的顺序不一致。排查死锁的经验是先看日志卡在哪个线程、哪一行然后看这个线程在等什么资源再看谁持有这个资源。用gdb加thread apply all bt就能看到所有线程的堆栈。举一个典型的错误例子std::mutex mtx1, mtx2; bool flag false; // 线程 1 void t1() { std::lock_guardstd::mutex lock1(mtx1); // 干很多事... std::lock_guardstd::mutex lock2(mtx2); // 这里再拿 mtx2 } // 线程 2 void t2() { std::lock_guardstd::mutex lock1(mtx2); // 干很多事... std::lock_guardstd::mutex lock2(mtx1); // 这里再拿 mtx1 }两个线程加锁的顺序相反就有可能出现死锁。解决办法是保证所有线程以相同的顺序获取锁或者用std::lock一次性锁定多个互斥量让系统去处理加锁顺序。多线程越复杂越要把“锁获取顺序”当成一种设计规范固定下来而不是临场发挥。6.2 忙等看起来在跑其实在烧 CPU新手很容易写出所谓的“自旋等待”——用一个while循环不断检查标志位是否变化std::atomicbool done false; // 线程 1 void worker() { std::this_thread::sleep_for(std::chrono::seconds(1)); done true; } // 主线程 while (!done) { // 什么都不干就干等 }这段代码在功能上没错但它会让主线程在这 1 秒里疯狂空转打满一整个 CPU 核心。如果同时有好几个线程这么干机器负载会直接拉满所有任务都变慢。真正适合自旋的只有一种场景等待的时间极短纳秒级而且能确定锁很快会被释放。否则优先用条件变量、future、信号量这些真正会让出 CPU 的同步原语。6.3 伪唤醒条件变量最经典的问题伪唤醒spurious wakeup是条件变量的一个特性wait()返回时不代表你等的条件已经满足可能只是被系统“假”地叫醒了。如果代码里用if来判断条件伪唤醒就会导致线程在条件不满足的情况下继续执行。这就是为什么前面所有例子里我都坚持用while而不是if。这个习惯在 C、Java、Python 里都通用。你写出while(!condition) wait()这种结构就永远不用担心伪唤醒的问题。它是多线程编程中少数的“多写个单词就能避免灾难”的套路千万不要图省事。6.4 性能过度惩罚顺序执行不等于全局加锁最后说一个最容易被忽视的问题有些人听到“顺序执行”第一反应就是给所有共享资源加同一把锁。比如我用一个大锁把整个业务逻辑包起来所有线程排着队进临界区的确实现了“顺序执行”但并发性能也被直接清零。更合理的方式是缩小锁的粒度或者彻底换一种并发模型。比如用“并发计算 按序提交”模式让任务在锁外面各自并行跑只有最终提交到结果容器时短时间加锁。锁的持有时间越短系统吞吐量就越高。我在压测的时候见过一个经典的对比同样是 8 个线程处理 8 个分片全程大锁方案耗时 2.4 秒锁外并行加提交时加锁方案耗时 0.8 秒差距非常明显。写在最后的经验做了这么多年多线程开发我最大的体会是顺序执行从来不是目的而是业务正确性的手段。在拿到“需要顺序执行”这种需求时先别急着写锁、写条件变量先问自己三个问题——任务是必须一个跑完才能跑下一个还是可以并发处理只是结果需要有序这个顺序是全局强制的还是只要局部有序就可以接受业务逻辑里有没有可能通过调整架构让“顺序”问题自然消失前两个问题的答案决定了你选哪种设计模式第三个问题往往是高手和新手的分水岭。有时候把并发任务拆成独立的队列用中间件或数据库的特性去保序比在代码层和线程搏斗要省力得多。反过来有时候接受“局部有序”这个折中能省下惊人的复杂度。多线程实现的顺序执行本质上是“并发”和“确定”之间的一场平衡你手里的工具越多越知道什么时候该让线程并发什么时候该让它们排队这才是工程师真正的价值所在。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询