bigywater.tech
爬虫工程 · 17

线程池与 asyncio:抓取吞吐在什么并发量下拉开差距

“爬虫该用线程还是 asyncio”几乎是必争的话题。这篇不讲道理,在本机用 0.1 秒延迟的服务器测:并发量逐步加大,总耗时离理论下限有多远、纯客户端开销差多少、内存怎么涨。

615 对 698 ms
并发 200、1000 个请求,asyncio aiohttp 与线程池 requests 的总耗时(下限 500)
0.109 对 0.331 ms
服务器不延迟时,每个请求的客户端开销(aiohttp 对 requests)
3.3 对 18.8 MiB
并发 200 时 asyncio 与线程池比起点多占的内存

等待占主导时,两者一样

实测 · 本机执行输出
  理论下限 = 1000 / 并发 × 0.1 秒
      并发      理论下限    线程池 requests asyncio aiohttp
       1    4000 ms        4258 ms        4172 ms   (请求数 40)
      10    2000 ms        2168 ms        2149 ms   (请求数 200)
      50    2000 ms        2179 ms        2164 ms   (请求数 1000)
     100    1000 ms        1171 ms        1125 ms   (请求数 1000)
     200     500 ms         698 ms         615 ms   (请求数 1000)
     300     333 ms失败:ConnectionError         530 ms   (请求数 1000)
  说明:macOS 监听队列上限 128(kern.ipc.somaxconn),并发连接一次涌入太多时测试服务器会重置连接,所以并发更高的数据不可靠,没有测

服务器每个请求停 0.1 秒,并发 10 到 100 时,线程池和 asyncio 的总耗时都只比理论下限多一点点,互相之间差不到 5%。I/O 等待是主要成本时,并发模型不是瓶颈。并发 200 时才看出 83 ms 的差距;并发 300 时线程池一侧遇到了我的测试服务器的连接重置(macOS 监听队列上限 128),不能说明线程池撑不住 300。

每个请求的客户端开销

实测 · 本机执行输出
  线程池 requests:每个请求平均 0.331 ms(总耗时 993 ms)
  asyncio aiohttp:每个请求平均 0.109 ms(总耗时 328 ms)

服务器不延迟时,请求本身没有等待,剩下的是客户端 CPU 开销:线程池 requests 每个请求 0.331 ms,asyncio aiohttp 0.109 ms。这里换掉的是整套库,不只是线程对协程;要单独比较并发模型,需要在同一个库(比如 curl_cffi 的同步和异步版本)上做,我没有做。

内存

实测 · 本机执行输出
      并发       线程池 requests 峰值内存      asyncio aiohttp 峰值内存   (起点:导入库之后的内存)
      10                49.4 MiB                  47.1 MiB   (起点 46.4 / 46.5 MiB;比起点多出 3.0 / 0.5 MiB)
     100                56.5 MiB                  48.5 MiB   (起点 46.5 / 46.6 MiB;比起点多出 10.0 / 1.9 MiB)
     200                65.3 MiB                  49.7 MiB   (起点 46.5 / 46.4 MiB;比起点多出 18.8 / 3.3 MiB)

线程池里每多一个并发就多一个线程,峰值内存随并发线性增长:并发 200 时多出 18.8 MiB(约每线程 0.09 MiB)。asyncio 同样的并发只多出 3.3 MiB。这里的内存是 ru_maxrss(峰值常驻内存),且请求体很小;爬真实页面时解析结果占的内存会盖过这一项。

分步演示

线程池与 asyncio
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。服务器每个请求 time.sleep(0.1) 后返回 200 字节;线程池是 ThreadPoolExecutor(并发数)配合同一个 requests.Session(连接池大小等于并发数);asyncio 是 aiohttp(TCPConnector(limit=0))加 Semaphore(并发数);每个并发量各测 2 次取中位数,无延迟的开销测 3 次取中位数;内存在独立的子进程里用 resource.getrusage 测峰值
    • 没有验证:并发超过 200 的情形(测试服务器的监听队列限制了它);真实网络下的长连接和更大的响应体;CPU 密集的解析混在其中时的表现(后面有单独的文章);同一个库的同步版与异步版的对比(如 curl_cffi);Python 自由线程(no-GIL)构建下的线程池;uvloop 等替代事件循环
    京ICP备2026067502号-1京公网安备11010502063487号