Python 多进程、多线程、协程到底选哪个:一张决策图讲清
Python 多进程、多线程、协程到底选哪个:一张决策图讲清「这个任务我该用 threading、multiprocessing 还是 asyncio?」这是 Python 后端绕不开的问题。选错了轻则没加速,重则性能不升反降。今天不堆概念,直接从 GIL 讲起,给你一套能落地的决策标准。绕不开的 GILCPython 有个全局解释器锁(GIL):任一时刻只有一个线程在执行 Python 字节码。这决定了一切:CPU 密集任务(算哈希、图像处理、纯计算),多线程根本没用——GIL 让它们轮流跑,还多了切换开销。IO 密集任务(网络请求、读写文件、查数据库),线程等 IO 时会释放 GIL,别的线程能趁机跑,所以多线程有效。先记住这条分界线,后面全靠它做判断。用实验验证:CPU 密集下多线程无用importtimefromthreadingimportThreaddefcpu_task(n):# 纯计算,典型 CPU 密集s0foriinrange(n):si*ireturnsdefrun_threaded():t1Thread(targetcpu_task,args(10_000_000,))t2Thread(targetcpu_task,args(10_000_000,))t1.start();t2.start()t1.join();t2.join()starttime.perf_counter()run_threaded()print(f2 线程:{time.perf_counter()-start:.2f}s)starttime.perf_counter()cpu_task(10_000_000);cpu_task(10_000_000)print(f串行:{time.perf_counter()-start:.2f}s)跑出来两者耗时几乎一样,多线程甚至因为锁竞争略慢。这就是 GIL——CPU 密集别指望 threading。CPU 密集:上多进程多进程每个进程有独立解释器和独立 GIL,能真正跑满多核。frommultiprocessingimportPoolimporttimedefcpu_task(n):s0foriinrange(n):si*ireturnsif__name____main__:# Windows/macOS 必须加这句,否则子进程递归 importstarttime.perf_counter()withPool(processes4)aspool:pool.map(cpu_task,[10_000_000]*4)print(f4 进程:{time.perf_counter()-start:.2f}s)4 核机器上大约能到串行的 1/4 耗时。代价是:进程创建开销大,进程间传数据要序列化(pickle),别拿它处理海量小任务或传大对象。IO 密集:协程比线程更省IO 密集时,多线程可行,但每个线程要占 MB 级栈内存,开几千个就吃不消。协程(asyncio)在单线程内用事件循环调度,一个协程等 IO 时立刻切到另一个,几万并发也就几十 MB。importasyncioimportaiohttpasyncdeffetch(session,url):asyncwithsession.get(url)asresp:returnawaitresp.statusasyncdefmain(urls):asyncwithaiohttp.ClientSession()assession:# gather 并发发起所有请求,而不是一个个等tasks[fetch(session,u)foruinurls]returnawaitasyncio.gather(*tasks)urls[https://httpbin.org/delay/1]*100resultsasyncio.run(main(urls))# 100 个 1 秒请求,总共约 1 秒出头100 个各睡 1 秒的请求,串行要 100 秒,协程 1 秒多就跑完。关键前提:整条链路必须是异步库(aiohttp 而非 requests)。混进一个同步阻塞调用,整个事件循环就被卡死。协程里混了阻塞代码怎么办现实中总有非异步的库(比如某些数据库驱动)。别直接在协程里调用它,用run_in_executor丢到线程池:importasynciodefblocking_io():# 假设这是个只有同步版本的库调用importtime;time.sleep(1)returndoneasyncdefmain():loopasyncio.get_running_loop()# 阻塞调用扔进线程池,不堵事件循环resultawaitloop.run_in_executor(None,blocking_io)print(result)asyncio.run(main())这样事件循环还能继续调度其他协程,不会被同步调用拖死。决策标准不用记概念,照着问自己两个问题:任务是 CPU 密集还是 IO 密集?CPU 密集(计算为主)→multiprocessing,进程数 ≈ CPU 核数。IO 密集(等网络/磁盘为主)→ 往下看第 2 问。IO 密集下,并发量多大、能否全异步?并发几十上百、且有成熟异步库 →asyncio,最省资源。并发不高,或只有同步库、想少改代码 →ThreadPoolExecutor,简单直接。还有个偷懒但有效的组合:主体用 asyncio,遇到 CPU 密集片段用run_in_executor丢进ProcessPoolExecutor,IO 和计算各得其所。小结GIL 是一切选型的前提:它让多线程对 CPU 密集无效,对 IO 密集有效。CPU 密集 → 多进程(真并行,代价是进程开销和序列化)。IO 密集 高并发 全异步链路 → 协程(最省内存)。IO 密集 有同步库 / 并发不高 → 线程池(改动最小)。协程里绝不能出现同步阻塞调用,非用不可就run_in_executor丢线程池。记忆点:CPU 密集用进程,IO 密集用协程,拿不准或想省事就用线程池——一切先看 GIL。