1 import asyncio
2 import multiprocessing as mp
3 import time
4 from concurrent.futures.process import ProcessPoolExecutor
5 from typing import Dict, Any, List
6
7 import aiohttp
8
9 # ====================== 1. 全局配置管控 ======================
10 # 切换此开关测试不同的海量数据处理模式
11 USE_STREAMING = True # True:开启无锁异步队列流式控制(适合 10万+ 任务);False:批量开发
12
13 AIOHTTP_CONCURRENCY_LIMIT = 10 # 协程高并发连接限制
14 REQ_TIMEOUT = aiohttp.ClientTimeout(total=5) # 网络请求超时设置
15
16 # Python 跑 CPU 密集型多进程坚决不超卖物理核,留 1 个核给主协程做高并发网络调度
17 CPU_WORKERS = max(1, mp.cpu_count() - 1)
18 CPU_PARSE_TIMEOUT = 10.0 # 单个 CPU 任务超时熔断
19
20
21 # ====================== 2. CPU 密集同步计算 ======================
22 # 注意:在 spawn 启动模式下,被进程池调用的函数必须放在模块顶层
23 def cpu_intensive_parse(raw_data: Dict[str, Any]) -> str:
24 total = 0
25 # 模拟 OpenCV 切图、文本区域清洗或密集 JSON 组装
26 for i in range(1_000_000):
27 total += i * i
28 return f"Parsed_Result_{raw_data['url']}_{total}"
29
30
31 # ====================== 3. 单个任务的混合流水线 ======================
32 async def fetch_and_parse(
33 session: aiohttp.ClientSession,
34 cpu_pool: ProcessPoolExecutor,
35 url: str
36 ) -> Dict[str, Any]:
37 try:
38 # ---------------- 阶段 A: I/O 密集型操作 ----------------
39 # 此时当前协程挂起,Event Loop 去处理其他协程,完全非阻塞
40 async with session.get(url, timeout=REQ_TIMEOUT) as resp:
41 await resp.text()
42 raw_data = {'url': url, "status": resp.status, "data": "raw_html..."}
43
44 if raw_data["status"] != 200:
45 return raw_data
46
47 # ---------------- 阶段 B: CPU 密集型操作 ----------------
48 # 1. 同步提交到全局进程池,立即拿到 concurrent.futures.Future
49 pool_future = cpu_pool.submit(cpu_intensive_parse, raw_data)
50
51 # 2. 核心:使用 asyncio.wrap_future 将进程池的 Future 桥接为协程可以 await 的对象
52 # 3. 配合 asyncio.wait_for 实现纯异步的超时熔断,在此期间主事件循环处于完全活跃状态
53 parsed_res = await asyncio.wait_for(
54 asyncio.wrap_future(pool_future),
55 timeout=CPU_PARSE_TIMEOUT
56 )
57 return {"url": url, "status": 200, "parsed": parsed_res}
58 except asyncio.TimeoutError:
59 # 专门捕获阶段 B 的 CPU 计算超时
60 return {"url": url, "status": "failed", "error": f"CPU 解析超时超过 {CPU_PARSE_TIMEOUT}s"}
61 except Exception as e:
62 # 捕获网络、DNS、各种突发异常,确保单条任务失败不影响整条流水线
63 return {"url": url, "status": "failed", "error": str(e)}
64
65
66 # ====================== 4. 方案 A:流式消费者工人 ======================
67 async def queue_consumer_worker(
68 queue: asyncio.Queue,
69 session: aiohttp.ClientSession,
70 cpu_pool: ProcessPoolExecutor,
71 final_results: List[Dict[str, Any]]
72 ):
73 """
74 流式工人协程:只要队列里还有 URL 就源源不断地拿出来消费
75 并发的工人数量完全由 AIOHTTP_CONCURRENCY_LIMIT 控制,内存始终恒定
76 """
77 while True:
78 try:
79 # 非阻塞地从队列获取一个 URL
80 url = queue.get_nowait()
81 except asyncio.QueueEmpty:
82 # 队列空了,工人优雅退出
83 break
84
85 # 执行完整的 I/O + CPU 流水线
86 result = await fetch_and_parse(session, cpu_pool, url)
87 final_results.append(result)
88
89 # 告诉队列该任务处理完毕
90 queue.task_done()
91
92
93 # ====================== 5. 异步主编排入口 ======================
94 async def main_async():
95 # 模拟 20 个测试 url
96 urls = [f"https://www.baidu.com" for i in range(20)]
97
98 # 初始化全局唯一进程池
99 cpu_pool = ProcessPoolExecutor(max_workers=CPU_WORKERS)
100
101 # 统一管控 HTTP 连接池
102 connector = aiohttp.TCPConnector(limit=AIOHTTP_CONCURRENCY_LIMIT)
103
104 start_time = time.time()
105 final_results = []
106
107 print(f"[主协程] 启动 | 物理核心数: {mp.cpu_count()} | 分配进程池大小: {CPU_WORKERS}")
108 print(f"[主协程] 当前运行模式: {'方案A: 异步队列流式' if USE_STREAMING else '方案B: 批量并发'}")
109
110 async with aiohttp.ClientSession(connector=connector) as session:
111 if USE_STREAMING:
112 # ---------------- 方案 A:纯正的异步队列流式处理 ----------------
113 # 1. 初始化异步队列,并将文本装载进队列(只存字符串,10万条也只占几MB内存)
114 task_queue = asyncio.Queue()
115 for url in urls:
116 task_queue.put_nowait(url)
117
118 # 2. 同时启动固定数量的消费者工人
119 workers = [
120 asyncio.create_task(queue_consumer_worker(task_queue, session, cpu_pool, final_results))
121 for _ in range(AIOHTTP_CONCURRENCY_LIMIT)
122 ]
123
124 # 3. 挂起等待所有工人把队列里的任务全部啃完
125 await asyncio.gather(*workers)
126 else:
127 # ---------------- 方案 B:简单优雅的批量并发 ----------------
128 tasks = [fetch_and_parse(session, cpu_pool, url) for url in urls]
129 # gather 会并发调度所有任务,并严格保持原数组顺序返回
130 final_results = await asyncio.gather(*tasks, return_exceptions=True)
131
132 # 优雅关闭进程池,cancel_futures=True 会自动取消队列中还没有来得及运行的物理进程任务
133 cpu_pool.shutdown(wait=True, cancel_futures=True)
134
135 # 结果统计展示
136 success_count = sum(1 for r in final_results if isinstance(r, dict) and r.get("status") == 200)
137
138 print("=" * 60)
139 print(f"[主协程] 全部任务执行完成!")
140 print(f"总处理条目: {len(final_results)} | 成功解析数: {success_count}")
141 print(f"系统总耗时: {time.time() - start_time:.2f} 秒")
142 return final_results
143
144
145 # ====================== 6. 顶层物理进程启动保护 ======================
146 if __name__ == "__main__":
147 # 强制使用 spawn 启动模式。在对接 vLLM、PyTorch 或 CUDA 时,
148 # 这是唯一能防止 CUDA 运行时死锁、守护进程分叉崩溃的安全模式。
149 mp.set_start_method("spawn", force=True)
150
151 # 启动 Asyncio 顶层事件循环
152 asyncio.run(main_async())