Python多进程、协程混合编程

  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())

 

posted @ 2022-08-17 18:18  yixiu868  阅读(74)  评论(0)    收藏  举报