Python 多线程与多进程并发编程实战

Python 并发编程绕不开 GIL,很多人被它坑过。这篇讲清多线程、多进程怎么选、怎么写,附爬虫和 CPU 密集型两个实战例子。

一、多线程:IO 密集型用 thread

import threading
import time

def download(url):
    print(f"开始下载: {url}")
    time.sleep(2)          # 模拟网络请求(IO等待)
    print(f"下载完成: {url}")

# 串行 6 秒
# 多线程并发 2 秒
start = time.time()
threads = []
for i in range(3):
    t = threading.Thread(target=download, args=(f"url-{i}",))
    t.start()
    threads.append(t)
for t in threads:
    t.join()               # 等所有线程结束
print(f"耗时: {time.time() - start:.1f}秒")
# 输出: 耗时约 2.0 秒

线程池版本(更推荐):

from concurrent.futures import ThreadPoolExecutor

urls = [f"url-{i}" for i in range(10)]

with ThreadPoolExecutor(max_workers=5) as pool:
    # 提交任务,返回 Future
    futures = [pool.submit(download, url) for url in urls]
    for f in futures:
        f.result()   # 阻塞等结果,异常会在这里抛出

二、GIL 是什么(必考)

GIL = 全局解释器锁

Python 同一时刻只有一个线程在执行 Python 字节码
→ 多线程无法利用多核 CPU

为什么有 GIL?
  CPython 内存管理(引用计数)不是线程安全的
  加 GIL 最简单,牺牲并行换安全

影响:
  多线程对 IO 密集型有效(线程等待IO时释放GIL)
  多线程对 CPU 密集型无效(只在一个核上跑)

验证 GIL:

# CPU 密集型任务,多线程反而更慢
def compute(n):
    return sum(i * i for i in range(n))

# 串行 vs 多线程,CPU 密集型下多线程没有优势
# 因为 GIL 让线程轮流占用 CPU,还有切换开销

三、多进程:CPU 密集型用 Process

from multiprocessing import Process, cpu_count
import os

def heavy_task(n):
    """CPU 密集型:大量计算"""
    result = 0
    for i in range(n):
        result += i * i
    print(f"进程 {os.getpid()} 计算完成: {result}")
    return result

if __name__ == "__main__":
    processes = []
    for i in range(cpu_count()):     # 进程数 = CPU核数
        p = Process(target=heavy_task, args=(5_000_000,))
        p.start()
        processes.append(p)
    for p in processes:
        p.join()

每个进程有独立的解释器和 GIL → 真正利用多核。 但注意进程间不共享内存,传数据用队列。

四、进程池 + 队列传数据

from multiprocessing import Pool, Queue
import multiprocessing

def worker(item):
    return item * item

def main():
    # 进程池:自动管理多个进程
    with Pool(4) as pool:
        results = pool.map(worker, range(10))
        print(results)   # [0, 1, 4, 9, ...]

    # 进程间通信用 Queue
    q = multiprocessing.Queue()
    q.put("数据")
    print(q.get())

if __name__ == "__main__":
    main()

五、异步 asyncio:更高的 IO 并发

IO 密集还能用协程,单线程也能上万并发:

import asyncio

async def fetch(url):
    print(f"请求: {url}")
    await asyncio.sleep(1)     # 模拟IO,协程挂起不占线程
    return f"数据: {url}"

async def main():
    tasks = [fetch(f"url-{i}") for i in range(5)]
    results = await asyncio.gather(*tasks)
    print(results)

asyncio.run(main())
# 5个请求 1 秒完成(并发),单线程

六、怎么选(核心结论)

任务类型方案原因
IO 密集型(爬虫/请求/读写)多线程 / asyncio等待IO时让出CPU,GIL不影响
CPU 密集型(计算/压缩/加密)多进程 ProcessPool绕开GIL,真正用多核
简单高并发IOasyncio 协程单线程上万并发,资源占用最小
# 推荐:concurrent.futures 统一接口
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor

# IO 密集 → 线程池
with ThreadPoolExecutor(max_workers=10) as pool:
    ...

# CPU 密集 → 进程池
with ProcessPoolExecutor(max_workers=4) as pool:
    ...

真实爬虫场景最佳实践:

import requests
from concurrent.futures import ThreadPoolExecutor

def crawl(url):
    resp = requests.get(url, timeout=5)
    return len(resp.content)

urls = ["https://example.com/" + str(i) for i in range(100)]

with ThreadPoolExecutor(max_workers=20) as pool:  # 20个并发请求
    sizes = list(pool.map(crawl, urls))
    print(f"总下载量: {sum(sizes)} 字节")
# 100个请求从串行100秒 → 并发约5秒

总结

Python 并发选择一句话:IO 密集用线程,CPU 密集用进程,超高并发用协程。

多线程 threading      → IO 等待时切线程,适合爬虫
多进程 multiprocessing → 绕开 GIL,适合计算
协程 asyncio          → 单线程高并发,最轻量

记住 GIL 的坑(CPU 密集别用多线程),再配合 concurrent.futures 统一接口,并发代码就能写得又快又稳。


💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值