ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

进程、线程、协程区别详解:从原理到并发选型实战

进程、线程、协程区别详解:从原理到并发选型实战 工作这几年我几乎每个星期都会被问到同一个问题“进程、线程、协程到底有什么区别” 问的人从刚入行的实习生到写了好几年业务代码的同事都有。大家之所以反复问是因为课本上的定义实在太“正”了——进程是资源分配的基本单位线程是调度的基本单位协程是用户态轻量级线程——背起来简单但回到项目里一写多线程、一调第三方库、一遇到卡顿和死锁整个人还是懵的。这篇文章我就用自己在实践中看到的真实现象把这些概念拆开讲。不堆术语尽量说人话。你学完以后至少能回答这几个问题为什么进程之间天然隔离、线程之间却会互相踩脚为什么异步代码有时候跑起来像单线程却能同时处理几万个连接以及所谓协程、虚拟线程、线程池、进程池这些东西到底应该在什么场景下选择哪一个。1. 模型从两张图看起操作系统眼里的人和代码眼里的人要理解进程、线程、协程不能只从某一个角度看。同样一个东西在操作系统眼里和在你写的代码眼里完全不是一回事。1.1 用“餐厅后厨”类比这三个概念想象你开了一家餐厅。程序就是菜谱它只是一堆静态的文字躺在那里什么也不干。进程是你把菜谱拿到后厨支起灶台、摆好锅碗瓢盆、占据了一块操作台开始真正炒菜。这个时候后厨里每一套灶具、每一样备好的食材都是这套操作独占的。别人不能随便来你的灶台上拿东西。每一个独立的餐厅分店都相当于一个进程。线程是什么呢你生意好了一个分店忙不过来于是你在一套灶具之外又多雇了几个厨师让他们共用这块操作台、共用这套锅碗瓢盆和食材。这几个厨师就是线程。他们之间配合得好翻台率极高但如果两个厨师同时去抢同一把锅铲那就会打起来。进程是大厨们共用的大厨房线程是厨房里同时干活的人。协程更像是单个厨师自己在脑内规划炒A菜的间隙把B菜焯水趁C菜炖着的时候去切D菜。他不需要额外的厨师不需要额外线程他只需要自己主动把“等待”的时间利用起来。自己决定什么时候把锅铲让出去、什么时候拿回来这叫协作式调度。这个类比虽然不完全严谨但足够帮你把框架搭起来。接下来我们往每个概念里填细节。1.2 三个概念的一句话版本进程操作系统分配独立资源内存、文件句柄、CPU时间的基本容器进程之间默认谁都不认识谁。线程进程内部的执行流共享进程的内存和资源多线程看着像同时在跑其实是操作系统在快速切换分配CPU时间片。协程完全工作在用户态的“执行流”它不依赖操作系统调度而是由程序自己主动让出和恢复执行位置所以切换成本极低但代价是同一时刻在同一个线程里真正执行的其实只有一个协程。先把这三句话记在心里后面所有问题都可以回到这三句话上验证。很多人之所以混淆是因为它们在不同语言、不同框架里长得不一样。比如Go语言的goroutine本质上就是协程的变体Java从21开始正式推出虚拟线程Virtual Thread底层也是类似协程的调度思想而Python的asyncio协程和C#的async/await全都属于这个家族。1.3 两个视角的差异如果你打开任务管理器、top、htop看到的“进程”是操作系统视角。你能看到每个进程占了多少内存、多少CPU、多少个句柄但你看不到它内部开了多少线程。如果你打开VisualVM、jstack、Process Explorer这类工具看到的是代码视角。你关心的是这个进程开了多少个线程、每个线程在等什么锁、哪个线程阻塞了。同一个进程在内部可以切出几十甚至几百个线程而每个线程又可能在特定时刻执行某个协程。搞明白这两种视角下面讲隔离、共享、通信、调度的时候你就不会觉得乱。2. 进程操作系统给你圈出来的一块独立地盘进程是三个概念里“分量最重”的一个。它负担着资源隔离和安全边界所以在它身上能看到操作系统几乎所有核心机制。2.1 从程序到进程PID、内存空间和“进程等待”一个程序要变成进程操作系统要做的事很多分配独立的内存地址空间、加载代码段和数据段、建立进程控制块PCB、分配文件描述符表、设置环境变量和启动参数最后给一个唯一的PID。为什么要有独立的地址空间这是最基础的安全设计。如果任何一个程序能直接读写别人的内存那一个崩溃的软件就能把整个系统拖垮。进程之间默认隔离的意义就是“你崩你的别带着我崩”。这里就引出第一个常见热词进程等待wait。在Linux下一个进程创建子进程之后如果父进程不做任何处理子进程结束时会变成僵尸进程Zombie占着一个PID和进程表项不释放。必须由父进程调用wait()或waitpid()来收尸进程才算真正清理完。很多服务器程序跑着跑着发现进程表被占满原因不是子进程还活着而是没人wait它们僵尸越积越多。我见过最典型的案例是某个定时任务脚本循环里用subprocess.Popen()启动外部程序却从来不communicate()或wait()。跑了几天之后系统进程数上千load average明明不高新进程就是创建失败。排查方式也简单ps -ef | grep defunct看到一堆僵尸把父进程改成waitpid(-1, WNOHANG)轮询清理就好了。2.2 守护进程与会话脱离终端的那些进程另外一个常见的迷惑点是守护进程与会话。你敲一条命令然后退出终端前台进程会跟着挂掉但有些进程比如我们常用的nginx、redis-server却能继续运行。原因是它们在启动时通过fork()创建了子进程并调用了setsid()建立新会话摆脱了控制终端。这个机制的现实意义是守护进程不依赖终端存在它只依赖内核的初始化系统init或systemd或者你手动用nohup、systemd service来管理。你写后台任务时如果想跑一个长驻进程正确的做法不是简单用放后台而是考虑它要不要成为守护进程、要不要写PID文件、要不要随系统自启。这些细节决定了你改天排查“进程明明在但日志不输出”“Procexp看它居然没有父进程”这类问题时能不能快速定位。2.3 修改进程名Linux下的小魔法热词里有“linux 修改进程名”和“linux 修改进程名 大于15个字符”这说明很多人实际碰到过这个问题。默认情况下进程名是ps里显示的COMMAND列它来自你启动程序的argv[0]。很多程序会在启动后根据配置文件重新设置argv[0]比如你用redis-server --port 6379启动ps里可能显示成redis-server *:6379这样的带参数形式这是redis自己做的。如果你想在自己的C/C程序里修改进程名老办法是直接改argv[0]简单粗暴但有效。但要注意Linux内核有读取限制新内核下comm字段也就是通常说的进程名最大是15个字符加结尾的\0超过就会被截断。你要想让ps显示更长更可读的名字得重新分配argv[0]对应的内存并写入新字符串。比较方便的是直接调用prctl(PR_SET_NAME, name)来设置comm字段但依旧受15字符限制如果只是想方便运维查看建议直接启动时把关键标识写在参数里更省事。这个细节看起来小但排查线上问题时很有用。我曾经维护过一套多实例服务每个实例都跑同样的二进制如果不修改进程名或启动参数ps -ef里全是一样的名字根本分不清哪个是哪个。后来统一启动脚本里带--rolexxx参数一眼认人。2.4 进程间通信IPC隔离之后的“外交手段”进程之间默认隔离那它们需要合作时怎么办这就是IPCInter-Process Communication的范畴。管道Pipe、命名管道FIFO、消息队列、共享内存、信号、Socket都算IPC。选择哪个取决于你的场景。管道是最简单的父子进程通信方式cmd1 | cmd2就是管道数据单向流动内核帮你做缓冲。命名管道则允许无亲缘关系的两个进程通信。共享内存是效率最高但最需要小心的方案两个进程直接读写同一块物理内存效率极高但丢失了进程隔离的好处必须自己用信号量或文件锁来同步否则并发写就是数据错乱。Socket是最通用的尤其适合跨机器通信——localhost上的进程通信本质也是走协议栈。明明有高共享内存、为什么还要持久保持进程隔离就是朴素的安全与稳定考虑。用IPC通信数据在边界上是显式的一方崩了另一方至少有机会知道。可如果是线程共享内存崩起来是连锁反应。这也是为什么Chrome要一个标签页一个进程、为什么数据库和Redis进程跑挂了不会直接拖挂业务进程的原因。2.5 进程池省掉重复创建的开销进程池也是一个高频搜索词。创建进程开销不小要复制地址空间、建立页表、初始化内核数据结构。如果任务是短而频繁的比如一个Python爬虫要同时爬几千个URL每次都fork一个子进程显然不现实。更合理的做法是在启动时就一次性创建固定数量的子进程比如4个或8个任务通过队列分发子进程处理完后把结果回传。Python的multiprocessing.Pool、Apache的worker进程模式、Nginx的worker process模型本质都是进程池。好处是进程个数稳定可控、彼此隔离、一个子进程崩溃不至于影响主管进程代价是你不能像线程那样直接共享大块内存数据跨进程传大对象要么序列化要么走共享内存。进程池特别吃内存的场景要小心。如果你通过fork()产生大量子进程而父进程又load了巨大的模型或数据集子进程的内存会看起来同样巨大。虽然Linux有写时复制Copy-on-Write机制并不是真复制了一份但你的子进程一旦动了这些数据就会触发物理内存复制8个子进程可能真的吃掉8份内存。勾选“fork方式启动进程池”之前先想想内存账。3. 线程同一个进程里的并行野心与秩序的代价进程解决了独立性问题但独立性带来的隔离也让共享变得困难。如果两个任务需要频繁共享一大批数据它们又都在同一个进程里那么线程就是合理的方案。3.1 线程是“轻量”的但不是无成本的线程之间共享什么共享进程地址空间、文件描述符表、信号处理器、工作目录。每个线程私有的只有自己的栈、寄存器上下文和线程局部存储TLS等极少数东西。所以线程的创建和切换代价远低于进程。创建成本的差异主要在不用重新分配独立的地址空间不用建立新的页表结构。上下文切换时线程只需要切换寄存器和栈指针而进程切换需要切换整个地址空间和页表缓存TLB代价更高。但代价低不代表免费。线程切换依然要陷入内核态由内核调度器来决定谁上CPU。一次线程切换的耗时通常在几微秒级别如果遇到高频率切换比如一个循环里反复加锁、解锁、切换这个开销会被放大得非常明显。我们常说“线程切换会泄漏吗”其实不是内存泄漏而是时间片被白白浪费掉了。用perf去分析高频并发程序时经常能看到大量时间花在调度器的上下文切换统计里。3.2 共享内存的美好与残酷互斥、锁和原子操作多线程不共享就没有意义共享就一定会遇到争用。两个线程同时对同一个变量执行i看起来是一行代码实际上至少是“读、加、写”三步。在并发场景下这两步之间完全可能被另一个线程插进来导致结果丢失更新。解决方式就是线程互斥用锁把临界区的访问串行化。Java里的synchronized、ReentrantLockC里的std::mutexPython里threading.Lock做的事情都一样同一时刻只允许一个线程进入临界区。那热词里的“atomicinteger线程安全吗”怎么答AtomicInteger在Java里是线程安全的但它保证的是单个操作的原子性比如incrementAndGet()是原子的。如果你组合使用比如先get()再比较再set()那就不再是原子的需要显式用compareAndSet来保证复合操作。很多人栽坑就栽在以为“用了Atomic就万事大吉”结果业务上有跨多个字段的状态校验Atomic也救不了你。原子性不等于业务上的整体一致性这个概念得刻在脑子里。3.3 死锁四个必要条件与最让人头疼的排查线程死锁是并发编程里最经典、最恶心的问题。死锁产生需要四个条件互斥条件、持有并等待、不可剥夺、循环等待。只要破坏任意一个死锁就不会发生。现实中我遇到过一个真实案例两个线程都持有锁A等待锁B而这种“双向等待”会造成两边永久卡死。Java里排查死锁的标准姿势是jstack pid它会直接打印出检测到的死锁并给出线程栈快照一眼就能看出谁持有谁的锁、在等谁的锁。如果用的是ReentrantLock还可以通过带超时的tryLock来打破死锁超过时间就放弃而不是无限期等待。避免死锁的工程经验是始终按同样的顺序获取多个锁。比如线程一要锁A再锁B线程二也必须锁A再锁B这样循环等待的条件就天然不成立了。代码审查时把“锁顺序一致性”作为红线能挡掉一大半死锁。3.4 线程池与阻塞队列并发版的“人员外包公司”线程的创建和销毁也不是免费的尤其是高并发的网络服务如果每个请求都开新线程系统早就被线程风暴打崩了。所以绝大多数服务端程序都用线程池。池子的意义在于提前创建好一组线程任务来了直接从池里取一个空闲线程运行任务完成线程归还池子避免了反复创建销毁的消耗。线程池里最需要花心思的是阻塞队列的选择。Java里几种常用的队列队列类型特点适用场景LinkedBlockingQueue链表实现默认无界可以设置容量任务量相对均匀适合无界排队但要注意内存堆积ArrayBlockingQueue数组实现有界容量固定需要限制排队积压避免任务无限堆积SynchronousQueue不存储任务直接交给线程要么立刻有人处理要么阻塞放任务适合处理线程非常多的场景DelayQueue延迟队列定时任务、延迟重试我在生产环境踩过最深的坑是线程池核心线程数设置太小而队列是无界队列任务进来不排队时显示看起来很正常但等到积压上千个任务时响应时间像坐了过山车。排查时第一反应居然是“是不是机器性能不够”后来看了JVM线程栈和队列长度才反应过来是队列和线程数的配合错了。经验法则是CPU密集型任务的线程数一般设为CPU核数1IO密集型则可以拉大线程数因为线程大部分时间在等待IO。不过真正稳妥的方式还是压测后用实际数据调整核心线程数和队列容量。3.5 守护线程与实际使用中的“杂症”守护线程在Java、Python里都有概念类似一个线程如果被标记为Daemon那么当所有非守护线程结束时虚拟机/进程会直接把守护线程一并终止不会等待它跑完。这个机制用来做后台清理、心跳检测非常合适但要注意它不能承载关键业务。我见过有人把消息发送线程设成Daemon结果主线程一退后台消息还没发完就全部丢光了。守护线程应该理解成“跟着主人走的小跟班”不是“可以自生自灭的野马”。热词里的“python线程嵌套线程”也很常见。Python里嵌套线程本身没问题一个线程在运行中开启另一个子线程子线程由父线程启动但归进程管辖。风险在于如果你在主线程里直接thread.join()等待子线程完成而子线程又可能等待另一个线程的结果代码的嵌套逻辑一旦复杂很容易出现“相互等待”的局面。写之前先画清楚依赖关系远比写完堆代码再调试节省时间。4. 协程不依赖操作系统的“自我调度”线程虽然在进程内部已经很轻量了但它的切换依然要经过内核。如果你需要极大规模并发比如单机同时维护十万个网络连接每个连接都要处理收发请求——用一万个线程就能把内存和CPU都吃光。这时候就轮到协程登场了。4.1 为什么说协程是“用户态自己调度自己”协程的运行和切换不经过操作系统调度器。程序自己保存上下文寄存器、栈指针并主动让出执行权操作系统眼中它始终只是一个普通的线程。所以协程切换的成本极低通常比线程切换低两个数量级。这带来的直接效果是你可以开出几万个甚至几十万个协程每个协程占用的空间远小于一个线程线程默认栈一般1MB以上协程初始栈几KB用动态增长还能再省。代价就是同一时刻跑在一个线程上的多个协程只有一个真正在CPU上执行。因为协程是“协作式”的它不会像线程那样被操作系统强占式调度它必须主动让出yield或await否则同一个线程里的其他协程就只能干等。这就是为什么协程里绝对不能写阻塞调用比如同步sleep、阻塞IO。阻塞了当前线程整个事件循环都会停顿。你在Python里用asyncio时如果不小心用了time.sleep()而不是await asyncio.sleep(0)你会看到整个程序莫名其妙卡住——因为它阻塞的是线程不是协程。4.2 事件循环协程的“调度中枢”协程通常和事件循环搭配使用。事件循环负责监听网络socket、定时器、信号等事件当某个事件触发时它就找到对应的协程恢复这个协程上一次挂起的位置继续执行。你写await的时候解释器看到的是当前协程执行到这里需要等待一个耗时操作于是它把自己挂起来并把控制权交还给事件循环。事件循环去忙其他协程等耗时操作完成再把控制权交还给这个协程。整个过程只发生在线程内部不涉及内核调度。这个模型在处理IO密集型任务时几乎是无敌的。一个线程、一个事件循环、几十万个协程吞吐量能轻松超过同样线程数线程模型的一百倍甚至更多。代价是原本运行在普通线程上的代码逻辑被撕裂成了“可以挂起的片段”你要小心管理共享状态、避免跨await持有锁还要注意任务取消的处理。4.3 Python协程到底是哪种写法热词“python协程”搜的人超多但很多教程一上来就把async、await、事件循环、Task讲得云里雾里。我给你拆到底层Python协程从3.5开始有async def语法从3.7开始有了官方推荐的asyncio.run()入口。你定义async def foo()它就生成一个协程对象在另一个协程里执行await foo()就是让当前协程在这里挂起等待foo()完成。如果需要并发执行多个协程则用asyncio.gather()或asyncio.create_task()把它们包装成任务Task交还给事件循环调度。想真实感受协程的切换最简单的例子是用两个async函数交替打印import asyncio async def a(): for i in range(3): print(fa-{i}) await asyncio.sleep(0.1) async def b(): for i in range(3): print(fb-{i}) await asyncio.sleep(0.1) async def main(): await asyncio.gather(a(), b()) asyncio.run(main())单线程、无操作系统调度但你能看到两个函数交替执行。这就是协程最直观的体现。4.4 虚拟线程JVM把协程思想“标准化”了Java的虚拟线程在JDK 21正式发布后底层实现非常接近协程思路虚拟线程是用户态调度的绑定在平台线程操作系统的线程上跑遇到IO阻塞时虚拟线程会与平台线程解绑让平台线程去跑别的虚拟线程。这样能让代码保持传统Thread写法的同步风格同时获得异步框架才有的高并发能力。虚拟线程的原理一句话概括把内核线程当作“搬运工”虚拟线程当作“货物”一个搬运工可以搬运海量货物。操作系统眼里你只是开了少量平台线程而在你的代码眼里你为每个请求创建了一个虚拟线程数量可达几万甚至几十万。使用限制也很明确如果你在虚拟线程里执行了System.exit()这类阻塞当前线程JVM的操作或者调用了synchronized这种会锁平台线程的代码虚拟线程可能会被钉住pinning这时候它的优势就消失了。所以想用好虚拟线程一定要避免在虚拟线程内部用synchronized改用ReentrantLock。5. 现实工程里的真实场景进程、线程、协程的选择逻辑讲了这么多原理最终要回答的还是那句话我的项目到底该用哪个5.1 “线程方程”的另一面算清楚账再选型热词里有个“线程方程组”我猜是大家在搜索并发模型设计时遇到的计算问题。其实这就是个分类讨论的账本机CPU密集型任务进程数或线程数压到CPU核数附近。网络IO密集型任务协程数可以开到几千几万线程池几十到几百。需要隔离崩溃风险的任务选进程。需要高频共享海量数据的任务选线程。需要写清晰同步代码、又要扛住高并发的任务选协程或者用成熟框架如Node.js、Go、Java虚拟线程帮你管理。事实上现在很多大型系统是混合模型可能是多个进程保证隔离与扩展性进程里跑线程池并行处理请求线程内部又用协程或异步IO处理单个请求的流式处理。你看Nginx就这么搞的主进程管理worker进程干活Redis用单线程事件循环也能扛高并发Go程序启动一堆goroutine但底层只用少数内核线程。5.2 三种对象对比表对比维度进程线程协程资源占用独立地址空间占用最大共享进程地址空间占用较小每个协程栈很小占用极小隔离性完全隔离崩溃互不影响共享内存崩溃可能带崩整个进程在同一个线程内隔离性最弱切换代价数微秒以上亚微秒到数微秒纳秒级甚至更低调度方操作系统操作系统程序自己用户态适合场景隔离要求高、需要多机部署CPU并行、需要共享数据高并发IO密集型、海量连接这张表简化了很多细节但决策时足够用了。5.3 CPU密集 vs IO密集选型的第一道工序我发现很多选型错误根源是没分清任务类型。CPU密集比如图像处理、编解码、大规模数值计算主线程瓶颈是计算能力。此时开几百个线程毫无意义因为CPU核数就那么多开多了反而因为频繁切换拖慢速度。这时候用进程池或者与CPU核数相当的线程池效果最好。IO密集比如请求第三方API、读写数据库、网络爬虫瓶颈是等待外部系统响应。此时线程或协程数量的价值体现出来了因为线程大部分时间在等待操作系统完全可以让其他线程/协程使用CPU这时候你开足够多的并发才能把CPU利用率“填满”。而协程的优势又在于数量可以开得极大不需要担心线程栈撑爆内存。5.4 从“能不能”到“值不值得”维护成本也是一种成本甚至技术在技术上完全可行工程上也未必应该选。线程模型虽然开发直觉简单但锁、死锁、竞态条件的调试成本高得惊人。协程模型虽然代码变成了async/await的样子调试和异常处理也都有学习曲线。进程模型虽然隔离性好但跨进程通信、序列化、运维部署都增加了复杂度。所以我的建议是大多数需要与外部IO打交道的业务优先考虑协程或异步模型需要吃满多核CPU的优先考虑线程或进程池对故障隔离有硬性要求的核心服务优先考虑进程。然后在团队掌握度和性能需求之间找一个平衡点技术选型永远没有免费的午餐。6. 实际排查中我遇到过的“进程线程协程周边问题”最后写几个真实踩坑记录。这些不一定是并发理论的核心但它们是你实际运行时最容易撞上的坑。6.1 文件被另一个进程锁定热词里的“F盘被另一个进程锁定”其实很常见。Windows下文件被某个进程独占打开的时候你想删除或者重命名就会提示“操作无法完成因为文件已在另一进程中打开”。这是由Windows文件锁机制引起的跟Linux下“允许删除打开的文件”完全不同。排查办法很简单用Process Explorer搜索文件句柄在菜单里选Find Handle or DLL输入文件名就能找到是哪个进程打开的。解决后是杀进程还是释放句柄看你自己的需要。另外如果你自己写代码记得养成打开文件后立刻释放的习惯尤其是写完数据库的备份、日志的轮转文件之后不释放锁属于最常见的事故源头。6.2 后台进程吃得CPU或内存太猛热词里有“msmpeng.exe占用过大”“msedgewebview2.exe进程如何关闭”这俩我遇到过太多次。msmpeng.exe是Windows Defender的杀毒扫描进程它会在你下载大量小文件或者编译项目时疯狂扫描IO路径CPU瞬时冲高。一般情况下不用管如果一直居高不下可以先把实时保护暂时关掉试试。msedgewebview2.exe是Edge浏览器组件很多软件内嵌了它来显示网页内容如果占用过高一般是某个软件的内嵌网页在跑东西只能从根源上找到是哪个软件或者关闭软件的内置网页功能。这类问题的排查方法论是不要急着杀进程先用任务管理器或Process Explorer看它的CPU/内存趋势再结合时间点判断是不是某个操作触发的。杀进程治标不治本还可能影响正常功能。6.3 终端进程启动失败conPTY错误另一个热词是“终端进程启动失败: 启动期间发生本机异常(无法启动 conpty)”。这是Windows下使用终端类工具比如VSCode集成终端的时候新版Windows Terminal/ConPTY组件出问题导致的。ConPTY是Windows为了兼容传统控制台程序而引入的伪终端机制。我遇到过的情况是更新系统之后ConPTY组件异常VSCode终端调不起来。解决顺序是先重启VSCode或重启系统如果不行就升级/修复Windows Terminal还不行就检查Windows更新是否异常。不要试图用旧版winpty凑合因为现在很多工具直接依赖ConPTY接口你被迫降级只会遇到更多兼容问题。6.4 Java的编译进程、JPS增量和OOM热词里还有“idea编译时,进程堆大小调整为8000,还是报错java: java.lang.outofmemoryerror: gc overhead limit exceeded”和“java: jps 增量注解进程已禁用”。这是两个不同的症状。第一个是JVM堆内存不够。把-Xmx调到8000MB也就是约8GB之后还报GC开销超限说明问题可能不只是堆小可能是代码里某个集合无脑添加数据导致内存泄漏或者元空间Metaspace配置不对。我在一次排查中发现某个项目循环往ArrayList里塞不落地的对象堆再大也会被撑爆。内存问题的排查还是要靠jmap堆转储和MAT分析而不是一味调大堆。第二个“jps增量注解进程已禁用”是IDEA编译时的一个提示说增量注解处理被关闭了部分重编译结果可能不准确。它本质是IDEA为了加速编译而做的优化如果你遇到编译结果异常优先执行一次完整Rebuild Project而不要长期依赖增量编译。这种提示大多不影响正常开发但你要知道它背后含义别一看到就慌。6.5 死锁复现与排查实操最后再给一个排查指南。假设你怀疑有死锁怎么快速定位Java直接jstack pid输出片段里搜索“Found one Java-level deadlock”下面会列出互相等待的线程栈。C/C用GDB attach上去thread apply all bt看所有线程的调用栈人工找循环等待关系。Python用faulthandler.dump_traceback_later()定时把线程栈全部dump下来看哪个线程卡在哪个锁上。Windows可以用Process Explorer的Threads页签查看线程栈配合!locks之类的WinDbg命令看锁状态。排查死锁的通用心法抓住线程栈先看每个线程正在等什么再看它手上持有什么然后画一条等待图循环等待一目了然。我自己每次排查这类问题都习惯在纸上画一张“谁持有什么、谁在等什么”的矩阵图比直接在IDE里抓瞎效率高得多。写到最后想分享一个我个人的切身体会无论你最终选了进程、线程还是协程都要把“并发不是越快越多越好”刻在心里。盲目堆并发不会让程序变快只会让资源在无意义的等待和切换中耗尽。真正的高手不是知道多少并发API而是清楚每一层抽象背后CPU、内存和调度器到底在替他承担什么。如果你看完这篇文章能对着自己的项目说出“我这里的瓶颈是IO还是CPU所以我选了协程/线程/进程”那它就没白写。
返回列表