我以为 Python 多线程没救了,直到发现 asyncio.to_thread()…真香!

开发
Asyncio.to_thread()让异步编程更灵活,既享受协程的高效,又能兼容阻塞代码。但它不是万能的,线程依然有GIL的限制,关键还是得根据场景选择方案。

作为一名Python开发者,我一度对多线程编程又爱又恨。爱的是它能提高程序效率,恨的是GIL(全局解释器锁)和各种死锁问题,搞得人头大。尤其是写异步代码时,遇到阻塞操作(比如文件IO、网络请求),整个事件循环都可能被卡住,简直让人抓狂!

直到Python 3.9带来了asyncio.to_thread(),我才发现——原来线程和异步还能这么玩?

1. 曾经的噩梦:阻塞操作卡死事件循环

以前写异步代码时,最怕遇到这样的情况:

import asyncio
import time

async def fetch_data():
    # 模拟一个阻塞操作(比如数据库查询)
    time.sleep(2)  # 啊哦,这里会卡住整个事件循环!
    return "Data fetched"

async def main():
    result = await fetch_data()  # 完蛋,整个程序停住了!
    print(result)

asyncio.run(main())
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

time.sleep()是同步阻塞的,直接调用会让整个asyncio事件循环卡住2秒,其他任务全得干等着。这显然不是我们想要的异步效果。

2. 旧时代的解决方案:run_in_executor

在Python 3.9之前,我们通常用loop.run_in_executor()把阻塞操作丢进线程池:

import asyncio
import time

def blocking_task():
    time.sleep(2)
    return "Done"

async def main():
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(None, blocking_task)  # 扔进线程池执行
    print(result)

asyncio.run(main())
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

虽然能用,但代码有点啰嗦,每次都要手动获取loop,而且run_in_executor的参数有点反直觉(第一个参数是executor,传None表示用默认线程池)。

3. Python 3.9的救星:asyncio.to_thread()

然后,Python 3.9带来了asyncio.to_thread(),让这一切变得超级简单:

import asyncio
import time

def blocking_task():
    time.sleep(2)
    return "Done"

async def main():
    result = await asyncio.to_thread(blocking_task)  # 一行搞定!
    print(result)

asyncio.run(main())
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

优点:

  • 代码更简洁:不用手动获取loop,直接await就行。
  • 语义更清晰:一看就知道是要把函数放到线程里跑。
  • 兼容性不错:虽然Python 3.9+才原生支持,但3.7~3.8也能用run_in_executor替代。

4. 适用场景:什么时候该用它?

asyncio.to_thread()最适合那些短时间、IO密集型的阻塞操作,比如:

  • 读写文件(open() + read())
  • 数据库查询(某些同步库如sqlite3、psycopg2)
  • 网络请求(requests库)
  • CPU计算(但如果是长时间计算,建议用multiprocessing)

但不适合:

  • 长时间CPU密集型任务(GIL会限制多线程性能,不如用多进程)。
  • 超高并发场景(线程太多会有调度开销,不如纯异步IO)。

5. 个人踩坑经验

刚开始用to_thread()时,我犯过一个错误:在一个协程里疯狂开几百个线程,结果系统资源直接炸了……

async def main():
    tasks = [asyncio.to_thread(blocking_task) for _ in range(1000)]  # 危险!瞬间开1000个线程!
    await asyncio.gather(*tasks)
  • 1.
  • 2.
  • 3.

后来学乖了,改用信号量(asyncio.Semaphore)控制并发:

async def run_with_limit(task_func, max_cnotallow=50):
    semaphore = asyncio.Semaphore(max_concurrency)
    async def wrapper():
        async with semaphore:
            return await asyncio.to_thread(task_func)
    return wrapper

async def main():
    tasks = [run_with_limit(blocking_task)() for _ in range(1000)]
    await asyncio.gather(*tasks)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

这样就能限制最大线程数,避免资源爆炸。

6. 总结:真香,但别滥用

asyncio.to_thread()让异步编程更灵活,既享受协程的高效,又能兼容阻塞代码。但它不是万能的,线程依然有GIL的限制,关键还是得根据场景选择方案:

  • 纯异步IO? 直接用aiohttp、asyncpg这类异步库。
  • 短阻塞操作? to_thread()真香!
  • 长时间CPU计算? 上multiprocessing吧。
责任编辑:赵宁宁 来源: 老猫coder
相关推荐

2020-11-05 11:10:43

程序员开发工具

2019-08-13 09:29:14

Kafka运营数据

2021-03-09 07:37:42

技术Promise测试

2020-08-13 10:15:34

MySQL数据库面试

2009-06-29 17:54:10

Java多线程Thread类创建线程

2018-09-06 14:18:05

硬盘数据恢复

2019-07-15 16:35:43

MySQL索引阿里

2022-08-29 10:52:37

线程函数操作系统

2020-08-26 10:03:31

MySQL索引

2020-11-04 09:38:05

GitHub代码开源

2021-08-04 07:57:17

C++多线程算法

2023-03-28 13:01:20

GPT-4开发OpenAI

2020-06-22 13:48:08

SQL查询SELECT

2019-10-30 21:27:51

Java中央处理器电脑

2010-03-17 14:58:20

Java多线程

2024-12-23 09:09:54

2010-02-01 17:25:09

Python多线程

2023-10-06 23:06:01

多线程Python

2021-04-13 16:05:02

程序员工具软件

2019-12-19 09:23:45

Java多线程数据
点赞
收藏

51CTO技术栈公众号