
为什么需要多线程单线程发送 100 个请求每个 0.5 秒总耗时 50 秒。多线程发送同样 100 个请求总耗时可能压到 3~5 秒。差距不是快一点是量级 difference。但多线程不是银弹。用错了比单线程还慢还容易把对方服务打挂。三种主流方案对比方案适用场景上手难度性能上限threading Queue简单批量任务⭐⭐中等concurrent.futures.ThreadPoolExecutor大多数场景首选⭐高asyncio aiohttp高并发、IO密集⭐⭐⭐⭐最高结论先给80% 的场景用ThreadPoolExecutor就够了。方案一threading Queue手动控制适合需要精细控制线程数、任务队列的场景。importthreadingimportqueueimportrequestsimporttime urls[fhttp://httpbin.org/delay/1?id{i}foriinrange(20)]result_queuequeue.Queue()defworker(q):whilenotq.empty():urlq.get()try:resprequests.get(url,timeout5)result_queue.put((url,resp.status_code))exceptExceptionase:result_queue.put((url,str(e)))finally:q.task_done()# 启动 5 个线程threads[]for_inrange(5):tthreading.Thread(targetworker,args(queue.Queue(),))t.start()threads.append(t)# 填入任务task_queuequeue.Queue()forurlinurls:task_queue.put(url)# 重新分配任务给 worker简化写法实际应把 task_queue 传进去fortinthreads:t.join()whilenotresult_queue.empty():print(result_queue.get())问题代码啰嗦手动管理线程生命周期容易写错。方案二ThreadPoolExecutor推荐Python 3.2 内置几行代码搞定。importrequestsfromconcurrent.futuresimportThreadPoolExecutor,as_completedimporttime urls[fhttp://httpbin.org/delay/1?id{i}foriinrange(20)]deffetch(url):try:resprequests.get(url,timeout5)returnurl,resp.status_codeexceptExceptionase:returnurl,str(e)t1time.time()withThreadPoolExecutor(max_workers5)asexecutor:futures{executor.submit(fetch,url):urlforurlinurls}forfutureinas_completed(futures):url,resultfuture.result()print(f{url}-{result})print(f耗时{time.time()-t1:.2f}s)优点自动管理线程池不用手动start/joinas_completed按完成顺序返回结果不用等全部完成max_workers控制并发数防止把对方打挂方案三asyncio aiohttp高性能适合上千级别并发或者你本身就在用异步框架FastAPI、Sanic 等。importasyncioimportaiohttpimporttime urls[fhttp://httpbin.org/delay/1?id{i}foriinrange(20)]asyncdeffetch(session,url):try:asyncwithsession.get(url,timeout5)asresp:returnurl,resp.statusexceptExceptionase:returnurl,str(e)asyncdefmain():asyncwithaiohttp.ClientSession()assession:tasks[fetch(session,url)forurlinurls]resultsawaitasyncio.gather(*tasks)forurl,resultinresults:print(f{url}-{result})t1time.time()asyncio.run(main())print(f耗时{time.time()-t1:.2f}s)优点单线程实现高并发资源占用极低。代价学习曲线陡所有调用链必须是 async 的混用会出问题。常见坑1. max_workers 不是越大越好workers 100 # ❌ 大概率更慢还可能被封 IP workers 10 # ✅ 大部分场景够用经验值10~20是 sweet spot。超过 50 基本没收益还可能触发对方限流。2. 忘了设 timeoutrequests.get(url)# ❌ 对方不响应你的线程就永远挂着requests.get(url,timeout5)# ✅ 5秒没响应就放弃3. 混用 Session 和多线程requests.Session()不是线程安全的。sessionrequests.Session()# ❌ 多线程共享同一个 session 会出问题# 正确做法每个线程自己创建 sessiondeffetch(url):withrequests.Session()ass:# ✅returns.get(url).text或者用ThreadPoolExecutor配合requests本身就没问题因为每个submit独立执行函数函数内自己创建 session。4. 对方有反爬多线程 高频率访问 容易触发风控。应对加随机延迟time.sleep(random.uniform(0.5, 2))换 User-Agent 池用代理 IP 池选型决策树请求量 100 → 单线程 requests 就够了别过度设计 请求量 100~1000 → ThreadPoolExecutor方案二 请求量 1000 或已在用异步框架 → asyncio aiohttp方案三 需要精细控制任务优先级/重试/失败队列 → threading Queue方案一或 Celery一句话总结多线程发送请求的核心不是开更多线程而是控制并发数 复用连接 设置超时。ThreadPoolExecutor解决了 80% 的问题剩下 20% 才需要上 asyncio 或 Celery。