I/O密集型场景下多线程比多进程慢的原因排查
多进程处理I/O密集任务反而更快的原因分析
你遇到的现象和常规认知有差异,结合你提供的代码,主要可以从以下几个角度解释:
1. 线程池并发数设置过低
你的多线程代码里ThreadPoolExecutor仅设置了max_workers=5,而multiprocessing.Pool默认会使用与CPU核心数一致的进程数(比如常见的8核CPU会启动8个进程)。I/O密集型任务的核心特点是线程/进程大部分时间在等待网络响应,所以并发数可以远高于CPU核心数——5个线程的并发能力远不如多进程的并发数,自然处理速度会慢很多。
2. 打印操作带来的锁竞争
多线程里的print是线程安全操作,底层会加全局锁。当大量线程同时执行print时,会频繁触发锁的获取与释放,产生额外的等待开销。而多进程的每个进程拥有独立的输出流,打印操作不会有跨进程的锁竞争,这部分开销被完全避免了。
3. GIL的隐性影响
虽然I/O操作会释放Python的全局解释器锁(GIL),但requests库的部分底层操作(比如DNS解析、某些C扩展逻辑)可能并没有完全释放GIL,或者GIL的切换过程本身带来了额外开销。多进程完全绕开了GIL的限制,每个进程拥有独立的Python解释器,不会受GIL约束。
4. 会话复用的细节差异
多线程用thread_local给每个线程分配一个Session,多进程则是每个进程一个Session。两者都实现了连接复用,但进程级的Session在某些网络场景下(比如服务器端的连接池策略)可能表现更优,不过这一点的影响相对前几点要小。
验证建议
- 把多线程的
max_workers调整到30或50,再对比测试结果; - 注释掉两个版本中的
print语句,消除打印带来的开销后重新运行; - 用
multiprocessing.cpu_count()查看多进程Pool的默认大小,确认并发数的差异。
你的测试代码
多进程代码
import requests import multiprocessing import time session = None def set_global_session(): global session if not session: session = requests.Session() def download_site(url): with session.get(url) as response: name = multiprocessing.current_process().name print(f"{name}:Read {len(response.content)} from {url}") def download_all_sites(sites): with multiprocessing.Pool(initializer=set_global_session) as pool: pool.map(download_site, sites) if __name__ == "__main__": sites = [ "https://www.jython.org", "http://olympus.realpython.org/dice", ] * 300 start_time = time.time() download_all_sites(sites) duration = time.time() - start_time print(f"Downloaded {len(sites)} in {duration} seconds")
多线程代码
import concurrent.futures import requests import threading import time thread_local = threading.local() def get_session(): if not hasattr(thread_local, "session"): thread_local.session = requests.Session() return thread_local.session def download_site(url): session = get_session() with session.get(url) as response: print(f"Read {len(response.content)} from {url}") def download_all_sites(sites): with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: executor.map(download_site, sites) if __name__ == "__main__": sites = [ "https://www.jython.org", "http://olympus.realpython.org/dice", ] * 300 start_time = time.time() download_all_sites(sites) duration = time.time() - start_time print(f"Downloaded {len(sites)} in {duration} seconds")
内容的提问来源于stack exchange,提问作者Sushmit Chakraborty
相关产品推荐
相关产品推荐

