Day 13:多进程与多线程

🎯 学习目标:理解并发/并行概念,掌握进程和线程的创建与管理

1. 同步/异步、并发/并行

1.1 概念辨析

概念含义生活类比
同步一件事做完再做下一件排队买票
异步不等待结果,先做其他的点了外卖先去工作
并发单核 CPU 快速切换任务(看起来同时)一个人边煮饭边回消息
并行多核 CPU 真正同时执行多个人同时做不同的事

graph TB

    subgraph SGxpzx3["并发(单核交替)"]

        T1a[任务A] -.-> T1b[任务B] -.-> T1c[任务A] -.-> T1d[任务C]

    end

    subgraph SG0kn3l["并行(多核同时)"]

        T2a[任务A] & T2b[任务B] & T2c[任务C]

    end

2. 多进程 multiprocessing

2.1 创建进程

 
import multiprocessing as mp
 
import time, os
 
def worker(name, duration):
 
    """模拟工作任务"""
 
    print(f"进程 {name}(PID={os.getpid()})开始工作")
 
    time.sleep(duration)
 
    print(f"进程 {name} 完成")
 
# 方式一:直接创建
 
p1 = mp.Process(target=worker, args=("A", 2))
 
p2 = mp.Process(target=worker, args=("B", 1))
 
p1.start()          # 启动进程
 
p2.start()
 
p1.join()           # 等待进程结束
 
p2.join()
 
print("所有进程完成")
 
# 注意:必须在 if __name__ == "__main__": 下创建进程(Windows 必须)
 

2.2 自定义进程类

 
class MyProcess(mp.Process):
 
    def __init__(self, name, duration):
 
        super().__init__()
 
        self.name = name
 
        self.duration = duration
 
    def run(self):
 
        """进程启动后自动调用此方法"""
 
        print(f"{self.name} 开始,PID={os.getpid()}")
 
        time.sleep(self.duration)
 
        print(f"{self.name} 结束")
 
if __name__ == "__main__":
 
    p = MyProcess("Worker", 2)
 
    p.start()
 
    p.join()
 

2.3 进程池 Pool

 
def compute_square(n):
 
    """CPU 密集型任务"""
 
    time.sleep(0.1)  # 模拟计算
 
    return n ** 2
 
if __name__ == "__main__":
 
    with mp.Pool(processes=4) as pool:
 
        # map:类似内置 map,但并行执行
 
        results = pool.map(compute_square, range(10))
 
        print(results)  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
 
        # apply_async:异步提交单个任务
 
        result = pool.apply_async(compute_square, (100,))
 
        print(result.get())   # 10000(阻塞等待结果)
 

2.4 进程间通信(Queue)

 
def producer(q):
 
    """生产者:向队列放数据"""
 
    for i in range(5):
 
        q.put(f"数据-{i}")
 
        print(f"生产了:数据-{i}")
 
        time.sleep(0.5)
 
    q.put(None)   # 结束信号
 
def consumer(q):
 
    """消费者:从队列取数据"""
 
    while True:
 
        data = q.get()
 
        if data is None:    # 收到结束信号
 
            break
 
        print(f"消费了:{data}")
 
if __name__ == "__main__":
 
    q = mp.Queue()
 
    p1 = mp.Process(target=producer, args=(q,))
 
    p2 = mp.Process(target=consumer, args=(q,))
 
    p1.start()
 
    p2.start()
 
    p1.join()
 
    p2.join()
 

3. 线程 threading

3.1 创建线程

 
import threading
 
import time
 
def task(name, delay):
 
    print(f"线程 {name} 开始")
 
    time.sleep(delay)
 
    print(f"线程 {name} 结束")
 
# 创建线程
 
t1 = threading.Thread(target=task, args=("T1", 2))
 
t2 = threading.Thread(target=task, args=("T2", 1))
 
t1.start()
 
t2.start()
 
t1.join()
 
t2.join()
 
print("所有线程完成")
 

3.2 线程池 ThreadPoolExecutor(推荐)

 
from concurrent.futures import ThreadPoolExecutor, as_completed
 
import requests
 
def fetch_url(url):
 
    """下载网页"""
 
    resp = requests.get(url, timeout=5)
 
    return url, resp.status_code
 
urls = [
 
    "https://www.python.org",
 
    "https://www.github.com",
 
    "https://www.baidu.com",
 
]
 
# 线程池
 
with ThreadPoolExecutor(max_workers=3) as executor:
 
    # 方式一:submit + future
 
    futures = {executor.submit(fetch_url, url): url for url in urls}
 
    for future in as_completed(futures):
 
        url, status = future.result()
 
        print(f"{url}{status}")
 
    # 方式二:map
 
    # results = executor.map(fetch_url, urls)
 

4. 线程不安全问题 ⚠️

4.1 竞态条件

 
# ⚠️ 有问题的代码
 
counter = 0
 
def increment():
 
    global counter
 
    for _ in range(100000):
 
        counter += 1  # 这不是原子操作!
 
        # 实际分三步:1.读值 2.加1 3.写回
 
if __name__ == "__main__":
 
    threads = [threading.Thread(target=increment) for _ in range(10)]
 
    for t in threads: t.start()
 
    for t in threads: t.join()
 
    print(f"期望:1000000,实际:{counter}")
 
    # 实际结果可能小于 1000000!
 

graph LR

    T1[线程1:读 counter=0] --> T1a[线程1:counter+1=1]

    T2[线程2:读 counter=0] --> T2a[线程2:counter+1=1]

    T1a --> M1[线程1:写回 counter=1]

    T2a --> M2[线程2:写回 counter=1]

    M2 --> |"两个线程各加了一次<br/>但 counter 只加了 1!"| Bug[❌ Bug]

4.2 加锁解决

 
counter = 0
 
lock = threading.Lock()
 
def increment_safe():
 
    global counter
 
    for _ in range(100000):
 
        with lock:           # 获取锁,同一时刻只有一个线程能进入
 
            counter += 1
 
if __name__ == "__main__":
 
    threads = [threading.Thread(target=increment_safe) for _ in range(10)]
 
    for t in threads: t.start()
 
    for t in threads: t.join()
 
    print(f"全部完成后:{counter}")   # 1000000 ✓
 

5. GIL 与选型指南

5.1 什么是 GIL

GIL(Global Interpreter Lock,全局解释器锁)是 CPython 的限制:同一时刻只有一个线程在执行 Python 字节码


graph TB

    subgraph CPython GIL

        T1[线程1] --> GIL_LOCK[🔒 GIL锁] --> CPU[CPU 核心]

        T2[线程2] --> GIL_LOCK

        T3[线程3] --> GIL_LOCK

    end

5.2 GIL 的影响

任务类型多线程多进程说明
IO 密集型(网络、文件、爬虫)✅ 推荐❌ 太重GIL 在 IO 时释放,多线程有效
CPU 密集型(计算、图像处理)❌ 无效✅ 推荐GIL 导致多线程无法利用多核

5.3 决策树

 
# 决策逻辑:
 
if task_is_io_bound:          # IO 密集型
 
    use_multithreading()       # → 线程(轻量、共享内存)
 
elif task_is_cpu_bound:        # CPU 密集型
 
    use_multiprocessing()      # → 进程(绕过 GIL)
 
elif need_truly_concurrent:    # 真正需要并发
 
    use_asyncio()              # → 异步(Day 22 会讲)
 

经验法则

  • 爬虫/下载/读写文件 → 多线程
  • 数学计算/图像处理/模型训练 → 多进程或异步
  • 简单并发 → 线程池 ThreadPoolExecutor

速记卡(面试闪卡)

Q1:一句话讲清「Day 13:多进程与多线程」到底是什么?

A:并发是单核交替看起来同时,并行是多核真同时;多进程独立内存适合CPU密集,多线程共享内存受GIL限制适合IO密集——按任务类型选。

Q2:并发 vs 并行:看起来 vs 真同时 —— 怎么理解?

A:并发(Concurrency)像一个人数着煮饭边回消息,是单核快速切换”看起来同时”;并行(Parallelism)是多个人真同时干不同事,靠多核。并发解”等”,并行解”算不动”。

Q3:多进程:各开一间房 —— 怎么理解?

A:多进程(Multiprocessing)像每人开一间独立房间,内存不共享、互不干扰,适合 CPU 密集型(计算多)。代价是进程切换和通信(IPC)贵,Windows 下必须放 main 里创建。

Q4:多线程:同屋共用客厅 —— 怎么理解?

A:多线程(Multithreading)像多人同住一套房共用客厅(共享内存),适合 IO 密集型(等网络/磁盘)。但 Python 有 GIL(Global Interpreter Lock,全局解释器锁),同一时刻只有一个线程跑字节码。

Q5:GIL:Python 的单车道 —— 怎么理解?

A:GIL(Global Interpreter Lock)是 CPython 的一把全局锁:哪怕多核,同一时刻只允许一个线程执行 Python 字节码。所以多线程在 CPU 密集上无法提速,IO 密集才因等待释放锁而有效。

Q6:核心速记主线有哪些?

  • 并发单核交替,并行多核真正同时

  • 多进程独立内存,适合 CPU 密集

  • 多线程共享内存,受 GIL 限,适合 IO 密集

  • CPU 密集用多进程,IO 密集用多线程/协程

口诀

A:并发交替并行真

进程独立吃CPU

线程同屋受GIL

IO密集它最行

相关链接


技术学习路线图 > 进阶特性