Skip to content

34|并发基础

程序同时处理多个任务时,有两种并发方式:多线程和多进程。线程在同一个进程内共享内存,切换开销小;进程有独立的内存空间,能绕开 Python 的 GIL(全局解释器锁),适合 CPU 密集型任务。

一、GIL

Python 的 CPython 实现有一个全局解释器锁(GIL),它确保同一时刻只有一个线程在执行 Python 字节码。这意味着:

  • 多线程不能并行执行 Python 代码
  • 多线程适合 I/O 密集型任务(网络请求、文件读写、等待用户输入)
  • CPU 密集型任务用多进程才能真正并行

GIL 只影响 CPython。Jython、IronPython 等其他实现没有 GIL,但使用较少。

二、threading

创建线程:

python
import threading
import time

def worker(name):
    time.sleep(1)
    print(f"{name} 完成")

# 创建并启动线程
t1 = threading.Thread(target=worker, args=("线程1",))
t2 = threading.Thread(target=worker, args=("线程2",))

t1.start()
t2.start()

t1.join()   # 等待线程结束
t2.join()

target 是线程要执行的函数,args 是传给函数的参数(元组)。start() 启动线程,join() 等待线程结束。

三、线程池

手动管理线程麻烦。concurrent.futures.ThreadPoolExecutor 自动管理线程池:

python
from concurrent.futures import ThreadPoolExecutor, as_completed
import time

def fetch(url):
    time.sleep(1)
    return f"{url} 的数据"

urls = ["url1", "url2", "url3", "url4"]

with ThreadPoolExecutor(max_workers=3) as executor:
    futures = {executor.submit(fetch, url): url for url in urls}

    for future in as_completed(futures):
        url = futures[future]
        try:
            result = future.result()
            print(f"{url}: {result}")
        except Exception as exc:
            print(f"{url} 出错: {exc}")

max_workers 控制同时运行的线程数。as_completed() 按完成顺序返回结果,不是按提交顺序。

四、线程安全

多个线程同时读写共享数据时,需要同步机制:

python
import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(10000):
        with lock:
            counter += 1

threads = [threading.Thread(target=increment) for _ in range(5)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print(counter)   # 50000

没有 lock 时,多个线程同时读取和修改 counter,最终结果可能小于 50000(竞争条件)。with lock: 确保同一时刻只有一个线程执行 counter += 1

五、多进程

CPU 密集型任务(如大量计算、数据处理)用多进程:

python
from concurrent.futures import ProcessPoolExecutor

def is_prime(n):
    if n < 2:
        return False
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            return False
    return True

numbers = [11227253509529, 112582705942171, 115280095190773]

with ProcessPoolExecutor(max_workers=3) as executor:
    results = executor.map(is_prime, numbers)
    for num, result in zip(numbers, results):
        print(f"{num}: {result}")

多进程的入口必须放在 if __name__ == "__main__": 下面,否则 Windows 上子进程导入模块时会递归创建进程。

六、选择线程还是进程

场景推荐方式
网络请求、文件读写、数据库查询多线程
大量计算、数据处理、图像处理多进程
CPU 和 I/O 混合根据瓶颈判断,或用 asyncio

七、asyncio 简介

asyncio 是 Python 的异步 I/O 框架,用单线程事件循环处理多个并发任务:

python
import asyncio

async def fetch(url):
    await asyncio.sleep(1)   # 模拟网络请求
    return f"{url} 的数据"

async def main():
    urls = ["url1", "url2", "url3"]
    tasks = [fetch(url) for url in urls]
    results = await asyncio.gather(*tasks)
    for result in results:
        print(result)

asyncio.run(main())

async def 定义协程,await 等待异步操作完成。asyncio.gather() 同时运行多个协程。

asyncio 适合大量 I/O 并发(如同时处理成千上万个连接),但写法与同步代码差异大。普通脚本中线程池通常足够。

记忆锚点:CPython 有 GIL,多线程不能并行执行 Python 代码;多线程适合 I/O 密集型,多进程适合 CPU 密集型;ThreadPoolExecutor 自动管理线程池;共享数据加 Lock 保护;多进程入口放 if __name__ == "__main__" 下;asyncio 适合大量 I/O 并发,但学习成本高。