
在深入代码实现前,我们首先厘清三种并发模式的底层逻辑,这是理解性能差异的基础:
线程是操作系统调度的基本单位,多线程通过在一个进程内创建多个执行流实现并发。Python 中的threading模块基于操作系统原生线程实现,但受GIL(全局解释器锁) 限制,同一时刻只有一个线程能执行 Python 字节码。这意味着 CPU 密集型任务无法通过多线程实现真正并行,但 I/O 密集型的爬虫场景(网络请求等待占比超 90%)中,线程切换能有效利用等待时间,提升整体效率。
进程是资源分配的基本单位,多进程通过multiprocessing模块创建独立的 Python 解释器进程,每个进程拥有独立的 GIL,可充分利用多核 CPU 资源。进程间通过管道、队列等机制通信,开销高于线程,但能突破 GIL 限制,适合 CPU 与 I/O 混合密集型的爬虫场景(如爬取后需即时解析数据)。
异步协程基于事件循环(Event Loop)实现,通过asyncio+aiohttp组合,在单线程内通过非阻塞 I/O 调度任务。协程的切换由程序自身控制(用户态),无需操作系统内核参与,切换开销远低于线程 / 进程,是纯 I/O 密集型爬虫的最优解。
为保证对比的公平性,所有测试基于以下统一环境:
python
运行
import requests
import time
def fetch(url):
"""单次请求函数"""
try:
response = requests.get(url, timeout=5)
return response.status_code
except Exception as e:
return str(e)
def sync_crawl(url, times):
"""同步爬虫主函数"""
start_time = time.time()
results = []
for _ in range(times):
results.append(fetch(url))
end_time = time.time()
total_time = end_time - start_time
print(f"同步爬虫完成{times}次请求,总耗时:{total_time:.2f}秒")
return total_time, results
if __name__ == "__main__":
TEST_URL = "https://httpbin.org/get"
REQUEST_TIMES = 1000
sync_crawl(TEST_URL, REQUEST_TIMES)核心说明:同步爬虫按顺序执行每个请求,前一个请求完成后才开始下一个,是性能对比的基准线。
python
运行
import requests
import threading
import time
from queue import Queue
# 线程安全队列,用于存储结果
result_queue = Queue()
def thread_fetch(url):
"""线程执行的请求函数"""
try:
response = requests.get(url, timeout=5)
result_queue.put(response.status_code)
except Exception as e:
result_queue.put(str(e))
def thread_crawl(url, times, thread_num=10):
"""多线程爬虫主函数"""
start_time = time.time()
threads = []
# 创建并启动线程
for _ in range(thread_num):
t = threading.Thread(target=lambda: [thread_fetch(url) for _ in range(times//thread_num)])
t.start()
threads.append(t)
# 处理剩余请求(整除余数)
remaining = times % thread_num
if remaining > 0:
for _ in range(remaining):
thread_fetch(url)
# 等待所有线程完成
for t in threads:
t.join()
end_time = time.time()
total_time = end_time - start_time
results = [result_queue.get() for _ in range(times)]
print(f"多线程爬虫({thread_num}线程)完成{times}次请求,总耗时:{total_time:.2f}秒")
return total_time, results
if __name__ == "__main__":
TEST_URL = "https://httpbin.org/get"
REQUEST_TIMES = 1000
thread_crawl(TEST_URL, REQUEST_TIMES, thread_num=10)核心说明:
threading.Thread创建指定数量的线程,平均分配请求任务Queue实现线程安全的结果存储,避免多线程数据竞争python
运行
import requests
import multiprocessing
import time
def process_fetch(url, result_list):
"""进程执行的请求函数"""
try:
response = requests.get(url, timeout=5)
result_list.append(response.status_code)
except Exception as e:
result_list.append(str(e))
def process_crawl(url, times, process_num=8):
"""多进程爬虫主函数"""
start_time = time.time()
manager = multiprocessing.Manager()
# 进程间共享列表
result_list = manager.list()
processes = []
# 分配任务并创建进程
tasks_per_process = times // process_num
remaining = times % process_num
for i in range(process_num):
task_count = tasks_per_process + (1 if i < remaining else 0)
p = multiprocessing.Process(
target=lambda: [process_fetch(url, result_list) for _ in range(task_count)]
)
p.start()
processes.append(p)
# 等待所有进程完成
for p in processes:
p.join()
end_time = time.time()
total_time = end_time - start_time
print(f"多进程爬虫({process_num}进程)完成{times}次请求,总耗时:{total_time:.2f}秒")
return total_time, list(result_list)
if __name__ == "__main__":
TEST_URL = "https://httpbin.org/get"
REQUEST_TIMES = 1000
process_crawl(TEST_URL, REQUEST_TIMES, process_num=8)核心说明:
multiprocessing.Manager()创建进程间共享列表,解决进程通信问题python
运行
import asyncio
import aiohttp
import time
async def async_fetch(session, url):
"""异步请求函数"""
try:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as response:
return response.status
except Exception as e:
return str(e)
async def async_crawl(url, times):
"""异步爬虫主函数"""
start_time = time.time()
results = []
# 创建异步会话
async with aiohttp.ClientSession() as session:
# 创建任务列表
tasks = [async_fetch(session, url) for _ in range(times)]
# 并发执行所有任务
results = await asyncio.gather(*tasks)
end_time = time.time()
total_time = end_time - start_time
print(f"异步协程爬虫完成{times}次请求,总耗时:{total_time:.2f}秒")
return total_time, results
if __name__ == "__main__":
TEST_URL = "https://httpbin.org/get"
REQUEST_TIMES = 1000
# 运行异步主函数
total_time, results = asyncio.run(async_crawl(TEST_URL, REQUEST_TIMES))核心说明:
aiohttp替代requests(异步 HTTP 客户端),避免阻塞asyncio.gather()批量执行协程任务,实现真正的非阻塞并发爬虫类型 | 1000 次请求总耗时(秒) | 平均单次耗时(毫秒) | CPU 利用率 | 内存占用 |
|---|---|---|---|---|
同步爬虫 | 285.6 | 285.6 | 8% | 45MB |
多线程爬虫(10 线程) | 32.8 | 32.8 | 25% | 68MB |
多进程爬虫(8 进程) | 28.5 | 28.5 | 75% | 320MB |
异步协程爬虫 | 15.2 | 15.2 | 30% | 52MB |
asyncio.sleep(0.1)),避免目标服务器封禁 IP(推荐亿牛云代理)TCPConnector设置连接池大小,提升复用率:python
运行
# 异步爬虫连接池优化示例
connector = aiohttp.TCPConnector(limit=100) # 最大并发连接数
async with aiohttp.ClientSession(connector=connector) as session:
# 执行爬虫任务python
运行
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=5))
async def async_fetch(session, url):
# 原有请求逻辑原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。