当前位置:首页>python>Python 异步编程的 5 个暗坑,async/await 不是万能药

Python 异步编程的 5 个暗坑,async/await 不是万能药

  • 2026-10-11 07:49:44
Python 异步编程的 5 个暗坑,async/await 不是万能药

今天不聊花哨的技巧,只讲那些年我们亲身趟过的并发天坑,每一个都是实打实的教训,带好小本本,发车! 


坑 1:daemon 线程的突然终止

先看这段代码,猜猜输出是什么:

import threadingimport timedef background_task():    time.sleep(1)    with open('/tmp/result.txt', 'w') as f:        f.write('done')t = threading.Thread(target=background_task, daemon=True)t.start()# 主线程立即结束

你以为是 文件被写入?

实际上输出的是 文件可能没有被写入。

daemon 线程在主线程退出时会被强制终止,不会等待它完成。如果 daemon 线程正在执行 I/O 操作(写文件、发网络请求),可能在中途被杀死,导致数据不完整或文件损坏。daemon 线程适合做不重要的后台任务(心跳、监控),不适合做必须完成的工作。

正确写法:

import threadingdef background_task():    with open('/tmp/result.txt', 'w') as f:        f.write('done')t = threading.Thread(target=background_task)t.start()t.join()  # 等待线程完成

踩坑现场:后台线程写日志设成了 daemon,程序异常退出时最后几行日志丢了,排查问题没有线索。


坑 2:多进程中的全局变量

先看这段代码,猜猜输出是什么:

from multiprocessing import Processshared_list = []def worker(val):    shared_list.append(val)procs = [Process(target=worker, args=(i,)) for i in range(5)]for p in procs: p.start()for p in procs: p.join()print(shared_list)

你以为是 [0, 1, 2, 3, 4]?

实际上输出的是 []。

多进程不共享内存。每个子进程都有自己独立的内存空间,对全局变量的修改只在子进程内生效,不会反映到主进程。这和多线程完全不同——多线程共享同一进程的内存。要在多进程间共享数据,需要用 multiprocessing.Queue、multiprocessing.Manager 或共享内存。

正确写法:

from multiprocessing import Process, Managerdef worker(shared_list, val):    shared_list.append(val)with Manager() as manager:    shared_list = manager.list()    procs = [Process(target=worker, args=(shared_list, i)) for i in range(5)]    for p in procs: p.start()    for p in procs: p.join()    print(list(shared_list))

踩坑现场:多进程爬虫用全局列表收集结果,跑完发现列表是空的。


坑 3:threading 的 GIL 限制

先看这段代码,猜猜输出是什么:

import threadingimport timedef cpu_work():    total = 0    for i in range(10**7):        total += istart = time.time()t1 = threading.Thread(target=cpu_work)t2 = threading.Thread(target=cpu_work)t1.start(); t2.start()t1.join(); t2.join()print(f'多线程: {time.time()-start:.2f}s')start = time.time()cpu_work(); cpu_work()print(f'单线程: {time.time()-start:.2f}s')

你以为是 多线程快一倍?

实际上输出的是 多线程和单线程差不多甚至更慢。

CPython 有全局解释器锁(GIL),同一时刻只有一个线程能执行 Python 字节码。对于 CPU 密集型任务,多线程不会带来加速,反而因为线程切换开销可能更慢。多线程在 Python 中只适合 I/O 密集型任务(网络请求、文件读写等)。CPU 密集型任务要用 multiprocessing 或 concurrent.futures.ProcessPoolExecutor。

正确写法:

from multiprocessing import Processimport timedef cpu_work():    total = 0    for i in range(10**7):        total += istart = time.time()p1 = Process(target=cpu_work)p2 = Process(target=cpu_work)p1.start(); p2.start()p1.join(); p2.join()print(f'多进程: {time.time()-start:.2f}s')

踩坑现场:用多线程做图片处理想加速,发现根本没变快,查资料才知道 GIL 的存在。


坑 4:signal 只能在主线程中使用

报错信息:ValueError: signal only works in main thread

出问题的代码:

import signalimport threadingdef handler(signum, frame):    print('caught signal')def setup_in_thread():    signal.signal(signal.SIGTERM, handler)t = threading.Thread(target=setup_in_thread)t.start()t.join()

Python 的 signal 模块只能在主线程中使用,在子线程中注册信号处理器会抛出 ValueError。这是因为 Unix 系统信号是进程级别的,发送给主线程处理。如果需要在多线程程序中处理信号,应该在主线程中注册处理器,然后通过 Event 或 Queue 通知其他线程。

修复方法:

import signalimport threadingstop_event = threading.Event()def handler(signum, frame):    stop_event.set()  # 通知其他线程# 在主线程中注册signal.signal(signal.SIGTERM, handler)def worker():    while not stop_event.is_set():        pass  # 工作循环

坑 5:asyncio.run 嵌套调用

报错信息:RuntimeError: asyncio.run() cannot be called from a running event loop

出问题的代码:

import asyncioasync def inner():    return 42async def outer():    return asyncio.run(inner())  # 嵌套调用asyncio.run(outer())

asyncio.run() 会创建一个新的事件循环,但一个线程中只能有一个运行中的事件循环。在已经运行的协程中再调用 asyncio.run() 会报错。在 async 函数中调用其他协程应该用 await,不需要也不能用 asyncio.run()。这个错误在 Jupyter Notebook 中也很常见,因为 Jupyter 已经有一个运行中的事件循环。

修复方法:

import asyncioasync def inner():    return 42async def outer():    return await inner()  # 用 awaitresult = asyncio.run(outer())

以上就是今天的 5 个坑。如果你觉得太简单了,说明你运气好还没踩过。

你还踩过哪些让人抓狂的并发大坑?评论区尽情吐槽吧~ 

来个人儿吧,我快自闭了

下期预告:性能陷阱

最新文章

随机文章