关于Python:6. Python并发编程

并发编程的核心在于“同时处理多个任务”,常用于 网络请求加速、IO密集型任务提速、并行计算 等场景。下面是几个关键概念:

并发 vs 并行

概念含义场景举例
并发(Concurrency)多个任务在同一时间段交替进行(一个CPU核切换任务)单核 CPU 执行多任务
并行(Parallelism)多个任务在同一时刻同时进行(多个CPU核同时执行)多核 CPU 同时运行多个程序

进程 vs 线程

特性进程线程
定义程序的独立运行单位进程内的执行单元
内存空间独立共享进程内存
资源开销大(启动慢)小(启动快)
通信方式进程间通信(IPC)较复杂共享变量、易通信
稳定性一个崩溃不影响其他进程一个线程崩溃可能影响整个进程
调度操作系统调度操作系统调度

一、线程(Thread)

线程是进程中的一个执行单元,是 CPU 调度的最小单位。多个线程共享进程的资源(如内存、文件句柄)。

  • 共享内存:同一个进程的线程之间共享堆、全局变量。

  • 开销小、切换快:比进程更轻量,创建和销毁的代价低。

  • 协作性强:线程间通信更快,但也更容易引发同步问题(如竞争条件)。

比如你在一个浏览器窗口里,同时播放视频、加载评论、更新弹幕,这些任务可能是由多个线程在一个进程中并行完成的。

1. 线程的基本操作

创建线程的两种方式

方法一:使用 threading.Thread(target=...)

import threading

def worker():
    print("工作线程执行中")

# 创建线程
t = threading.Thread(target=worker)
t.start()  # 启动线程
t.join()   # 等待线程结束

方法二:继承 threading.Thread 类(更灵活)

class MyThread(threading.Thread):
    def run(self):  # 重写 run 方法
        print("自定义线程执行中")

t = MyThread()
t.start()
t.join()

2. start() 和 run() 的区别与原理

方法说明
start()通知系统创建一个新的线程,由系统调用 run() 方法(真正开了一个线程
run()是线程的具体逻辑函数,如果你直接调用它,就只是普通函数调用(不会创建新线程

示例对比:

import threading
import time

class MyThread(threading.Thread):
    def run(self):
        print(f"线程开始执行:{threading.current_thread().name}")
        time.sleep(1)
        print("线程执行完毕")

t = MyThread()

print("直接调用 run():")
t.run()  # 不会创建新线程,只是普通函数调用

print("调用 start():")
t.start()  # 真正开了一个新线程
t.join()

输出说明:

  • run():执行在主线程

  • start():开启新线程,由操作系统调度运行 run()

3. daemon(守护线程)

守护线程(daemon thread)随主线程退出而退出。主线程结束后,非守护线程必须执行完,守护线程可直接终止

非守护线程(用户线程)——主要工作线程 程序的“核心逻辑部分”。如:文件写入、数据下载等。

守护线程(后台线程)——辅助性工作线程 如日志记录、心跳检测、资源监控等,不是主功能。

import threading
import time

def daemon_worker():
    print("守护线程开始")
    time.sleep(5)
    print("守护线程结束")  # 不一定能执行到

t = threading.Thread(target=daemon_worker)
t.daemon = True  # 设置为守护线程
t.start()

print("主线程结束")  # 守护线程可能还没执行完

输出结果:

守护线程开始
主线程结束
(可能没有"守护线程结束")

注意:daemon 必须在 start() 之前设置!

4. join() 的作用

join() 会让主线程 等待该子线程执行完毕后再继续运行

示例:

import threading
import time

def worker():
    print("子线程开始")
    time.sleep(2)
    print("子线程结束")

t = threading.Thread(target=worker)
t.start()

t.join()  # 等子线程结束再往下走
print("主线程结束")

如果没有 join(),主线程可能在线程结束前就退出了。

5. 完整示例

import threading
import time

class MyThread(threading.Thread):
    def run(self):
        print(f"[{self.name}] 开始")
        time.sleep(2)
        print(f"[{self.name}] 结束")

# 普通线程
t1 = MyThread(name="普通线程")
t1.start()

# 守护线程
t2 = MyThread(name="守护线程")
t2.daemon = True
t2.start()

# 主线程等待 t1 完成,但不等守护线程
t1.join()
print("主线程结束")
操作说明
start()启动一个线程,由系统调用 run()
run()重写此方法定义线程行为,直接调用不会创建线程
join()阻塞当前线程,直到目标线程执行完毕
daemon=True设置线程为守护线程,随主线程退出而退出

6. 线程间通信:Queue

线程之间安全地传递数据,用 queue.Queue,线程安全。

import queue

q = queue.Queue()

def producer():
    for i in range(5):
        q.put(i)
        print(f"生产了 {i}")

def consumer():
    while True:
        item = q.get()
        if item is None: break
        print(f"消费了 {item}")

t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)

t1.start()
t2.start()
t1.join()
q.put(None)  # 终止信号
t2.join()

二、进程(Process)

Python 中的 进程(Process) 是并发编程的一种方式,尤其适合 CPU 密集型任务

  • 进程是操作系统资源分配的基本单位,每个进程拥有独立的内存空间。
  • Python 的多线程受限于 GIL(全局解释器锁),但多进程不受影响,能真正实现并行执行。
  • 独立性强:进程之间相互独立,一个进程崩溃不会影响其他进程。
  • 拥有完整资源:每个进程都拥有自己的堆栈、数据段、代码段。
  • 创建成本高:启动一个新进程需要较多的资源(如内存、时间)。
  • 程序是静态的代码,进程是程序的动态运行实例。

比如你打开两个浏览器,一个看视频,一个登录网页,这两个浏览器实例就是两个进程,互相独立运行。

1. Python 创建进程的方式

方法一:使用 multiprocessing.Process

from multiprocessing import Process
import time

def task(name):
    print(f'{name} started')
    time.sleep(2)
    print(f'{name} finished')

if __name__ == '__main__':
    p = Process(target=task, args=('进程1',))
    p.start()
    p.join()
    print("主进程结束")

方法二:继承 multiprocessing.Process

from multiprocessing import Process
import time

class MyProcess(Process):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        print(f'{self.name} 执行中')
        time.sleep(1)
        print(f'{self.name} 完成')

if __name__ == '__main__':
    p = MyProcess("子进程")
    p.start()
    p.join()

2. 常用操作详解

start() vs run()

  • start():启动新进程,系统调用 run() 方法

  • run():定义进程逻辑,直接调用不会启动新进程

join()

  • 阻塞主进程,直到子进程执行结束。

3. 进程间通信(IPC)

由于进程之间不共享内存,通信需要借助队列、管道、共享内存等机制

使用 Queue 实现通信:

from multiprocessing import Process, Queue

def worker(q):
    q.put("来自子进程的消息")

if __name__ == '__main__':
    q = Queue()
    p = Process(target=worker, args=(q,))
    p.start()
    print(q.get())  # 接收子进程传回来的数据
    p.join()

4. 进程池(Pool)

适用于大量任务并发处理,自动管理多个进程。

from multiprocessing import Pool
import time

def task(n):
    time.sleep(1)
    return n * n

if __name__ == '__main__':
    with Pool(4) as pool:  # 创建包含4个进程的进程池
        results = pool.map(task, [1, 2, 3, 4, 5])
        print(results)

map 会自动将任务分配给多个进程并收集结果。

5. 守护进程(daemon)

from multiprocessing import Process
import time

def worker():
    print("子进程启动")
    time.sleep(3)
    print("子进程结束")

if __name__ == '__main__':
    p = Process(target=worker)
    p.daemon = True  # 设置为守护进程
    p.start()
    print("主进程结束")  # 守护进程随主进程结束而终止

三、threading.local

threading.local() 是 Python 的一个特殊类,用来创建每个线程自己私有的变量空间
每个线程对它进行读写,不会影响到别的线程。

1. 为什么需要 threading.local

在多线程中,我们经常遇到全局变量被多个线程共享导致数据冲突的问题。

例子(共享全局变量):

import threading

var = {}

def worker(x):
    var['value'] = x
    print(f"{threading.current_thread().name}: {var['value']}")

for i in range(3):
    t = threading.Thread(target=worker, args=(i,))
    t.start()

你会看到输出是混乱的,因为线程间共享同一个 var

threading.local() 解决上述问题,改写的代码如下:

import threading

# 创建线程局部变量容器
local_data = threading.local()

def worker(x):
    # 为当前线程设置一个名为 value 的变量
    local_data.value = x
    # threading.current_thread(): 当前正在运行的线程对象
    print(f"{threading.current_thread().name}: {local_data.value}")

# 启动3个线程,每个线程传入不同的参数 i
for i in range(3):
    t = threading.Thread(target=worker, args=(i,))
    t.start()

每个线程都有自己的 local_data.value,互不影响!

2. 底层原理简要说明

threading.local() 会在每个线程内部创建一份独立的字典,存储这个线程对应的数据。

内部相当于:

thread_id = get_current_thread_id()
data_store[thread_id] = {"value": x}

3. 实战应用场景举例

日志系统中的上下文变量:

在多线程环境下,每个线程都处理不同的用户请求,你可能希望每个日志条目都带上“用户ID”:

import threading

user_context = threading.local()

def set_user(user_id):
    user_context.uid = user_id

def log(message):
    print(f"[用户 {user_context.uid}] {message}")

def handle_request(uid):
    set_user(uid)
    log("请求开始")

threads = []
for uid in range(3):
    t = threading.Thread(target=handle_request, args=(uid,))
    threads.append(t)
    t.start()

数据库连接管理器:

每个线程处理一个用户请求,不希望多个线程共用一个连接。

connections = threading.local()

def get_connection():
    if not hasattr(connections, "conn"):
        connections.conn = create_connection()
    return connections.conn
注意点说明
不共享不同线程设置的属性互不影响
和线程生命周期绑定线程结束后,对应的属性自动清除
不适合多进程threading.local() 只适用于多线程,不适用于 multiprocessing

测试代码:

import threading
import time

local_var = threading.local()

def f():
    local_var.val = threading.current_thread().name
    time.sleep(1)
    print(f"{threading.current_thread().name}: {local_var.val}")

threads = []
for i in range(5):
    t = threading.Thread(target=f, name=f"线程-{i}")
    threads.append(t)
    t.start()

4.总结

项目内容
类名threading.local()
作用创建线程私有的变量空间
每个线程访问的是自己的副本,互不干扰
应用场景日志上下文、数据库连接、线程用户数据等

四、threading.Event

Event 是一个线程同步原语,用于线程之间通过标志位进行通信
可以理解成:一个可以被“设置”或“清除”的信号量(开关)。

方法作用
set()把标志位设为 True,通知等待的线程
clear()把标志位设为 False
is_set()检查标志位是否为 True
wait()如果标志为 False,线程阻塞,直到 set() 被调用

1. 简单例子:等待一个“信号”

import threading
import time

event = threading.Event()

def worker():
    print("线程开始执行,等待信号...")
    event.wait()  # 等待 set()
    print("收到信号,线程继续执行")

t = threading.Thread(target=worker)
t.start()

time.sleep(2)
print("主线程2秒后发出信号")
event.set()

运行输出:

线程开始执行,等待信号...
主线程2秒后发出信号
收到信号,线程继续执行

2. 使用场景:线程控制 + 通信

主线程控制子线程启动/暂停:

event = threading.Event()

def worker():
    print("子线程准备中...")
    event.wait()  # 等待“启动”信号
    print("子线程开始干活")

event.clear()
t = threading.Thread(target=worker)
t.start()

time.sleep(3)
event.set()  # 通知子线程可以开始

多线程等待统一信号(发布-订阅)

event = threading.Event()

def listener(name):
    print(f"{name} 等待开门")
    event.wait()
    print(f"{name} 进门了")

for i in range(3):
    threading.Thread(target=listener, args=(f"线程-{i}",)).start()

time.sleep(2)
print("门打开(发送信号)")
event.set()

实现暂停/继续功能(配合 clear()

event = threading.Event()

def runner():
    while True:
        event.wait()  # 一直等着 resume 信号
        print("线程正在运行")
        time.sleep(1)

event.set()  # 初始为允许运行
t = threading.Thread(target=runner)
t.start()

time.sleep(3)
print("暂停线程")
event.clear()

time.sleep(3)
print("恢复线程")
event.set()
  • event.wait() 是阻塞的,直到你 event.set() 才继续。

  • 一旦 set() 被调用,所有调用 wait() 的线程都将继续执行。

  • 如果你想再次阻塞线程,需要用 clear() 重置。

3. 和锁(Lock)区别

对象功能适合场景
Lock用于互斥访问共享资源多个线程访问同一个变量时
Event用于线程间通信、同步调度控制线程启动、同步等待

4. 实战应用:爬虫控制系统中的“启动”信号

import threading
import time

event = threading.Event()

def spider_thread(name):
    print(f"{name} 等待管理员下达启动命令...")
    event.wait()
    print(f"{name} 正在抓取网页...")

for i in range(5):
    threading.Thread(target=spider_thread, args=(f"爬虫-{i}",)).start()

time.sleep(2)
print("管理员:全部启动")
event.set()

5. 总结

方法用途
event.wait()阻塞线程直到事件被 set
event.set()发出“允许执行”的信号
event.clear()将事件标志重置为 False,重新阻塞
event.is_set()检查当前事件状态(True/False)

Event 本质上是一个线程之间的“通知机制”,就像红绿灯一样控制线程的执行。


五、threading.Lock

方法说明
lock.acquire()手动加锁(阻塞)
lock.release()手动解锁
lock.acquire(timeout=3)最多等待3秒获得锁
with lock:推荐写法(自动获取和释放锁)

1. Lock 的使用与分析

threading.Lock 是最基本的互斥锁,用来防止多个线程同时访问共享资源

用法一:手动 acquire() / release()

lock = threading.Lock()
lock.acquire()
try:
    # 访问临界资源
finally:
    lock.release()

用法二:推荐的 with 上下文语法

with lock:
    # 安全访问共享资源

这相当于自动调用 acquire()release(),即便中间出错也会释放。

如果不加锁会怎样?

假设有两个线程同时执行 count += 1,它其实分为:

1. 读取 count 到寄存器
2. 加 1
3. 写回 count

两个线程可能会这样交错执行,导致最终结果不是预期的。

实验:加锁 VS 不加锁

import threading

count = 0
lock = threading.Lock()

def add():
    global count
    for _ in range(100000):
        with lock:
            count += 1

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

print("最终结果:", count)  # 应该是 200000

不加锁时,结果通常是 17~19 万。

2. Lock 的原理(内部机制)

GIL ≠ Lock

Python 的 GIL(全局解释器锁)确实控制线程执行,但 GIL 只保护解释器本身,不保护你写的共享变量。

所以多线程写变量必须用 Lock 手动控制。

Lock 内部实现(C 层)

  • Python 的 threading.Lock 实际上是 thread.allocate_lock() 实现的。

  • 对不同系统底层封装了系统原生的互斥锁(如 POSIX Mutex)。

  • 类似伪代码:

class Lock:
    def acquire(self):
        # 尝试获取锁,阻塞直到成功
    def release(self):
        # 释放锁,让下一个线程继续

死锁问题

死锁场景:两个线程拿住了对方想要的资源,彼此等待

lock1 = threading.Lock()
lock2 = threading.Lock()

def t1():
    with lock1:
        time.sleep(1)
        with lock2:  # 可能卡住
            pass

def t2():
    with lock2:
        time.sleep(1)
        with lock1:  # 也卡住
            pass

死锁示意图:

t1: 持有 lock1,等待 lock2
t2: 持有 lock2,等待 lock1
==> 永久等待,程序卡住不动

解决方法之一就是使用 可重入锁 RLock设置锁获取超时

3. RLock(可重入锁)

什么是 RLock?

  • RLock = Reentrant Lock(可重入锁)

  • 允许同一个线程多次获得同一个锁,不会死锁。

普通 Lock 的问题:

lock = threading.Lock()

def outer():
    with lock:
        inner()

def inner():
    with lock:  # 死锁:自己套自己
        print("Inner logic")

outer()

RLock 正确用法

lock = threading.RLock()

def outer():
    with lock:
        inner()

def inner():
    with lock:  # 允许同一线程重复加锁
        print("Inner logic")

outer()

内部原理:

  • RLock 内部维护了一个锁计数器和持有锁的线程 ID。

  • 第一次 acquire() 后计数 +1;

  • 每次 release() 计数 -1;

  • 只有计数归 0,才真正释放锁。

4. 实战案例:线程安全的计数器类

import threading

class SafeCounter:
    def __init__(self):
        self.count = 0
        self.lock = threading.Lock()

    def increment(self):
        with self.lock:
            self.count += 1

    def get(self):
        with self.lock:
            return self.count

counter = SafeCounter()

def worker():
    for _ in range(10000):
        counter.increment()

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

print("最终计数:", counter.get())

5. 总结

关键词含义
Lock多线程控制共享资源互斥访问的最基本工具
acquire/release显式加锁与释放
with lock推荐写法,防异常未释放
死锁多锁交叉等待,需避免
RLock可重入锁,解决递归调用时的锁问题

六、threading.Condition

Condition 是一个高级同步原语,允许多个线程基于某个状态进行等待和通知

典型应用场景:一个线程等待资源准备好,另一个线程通知资源已就绪。

  • Condition 是建立在 Lock(或 RLock)基础上的。

  • 提供了两个核心机制:

    • wait():挂起当前线程,等待其他线程通知。

    • notify() / notify_all():唤醒一个 / 所有等待的线程。

1. 基本用法示例

场景:消费者等待商品,生产者生产后通知

import threading
import time

# product_ready: 共享变量,表示产品是否准备好,初始为 False
product_ready = False
# condition: 一个 条件变量(Condition Object),用于线程之间通信和等待唤醒。
condition = threading.Condition()

def consumer():
    with condition:
        print("消费者:等待产品")
        while not product_ready:
            condition.wait()  # 阻塞,等待被唤醒
        print("消费者:收到产品,开始消费")

def producer():
    global product_ready
    time.sleep(2)  # 模拟生产耗时
    with condition:
        product_ready = True
        print("生产者:产品已生产,通知消费者")
        condition.notify()  # 唤醒等待的消费者

threading.Thread(target=consumer).start()
threading.Thread(target=producer).start()

2. Condition 的方法解析

方法说明
acquire() / release()底层锁的获取与释放(不常直接用)
wait(timeout=None)当前线程进入“等待队列”直到被唤醒
notify(n=1)唤醒最多 n 个等待线程(默认1个)
notify_all()唤醒所有等待线程

所有这些方法都必须在加锁状态下调用,否则会抛异常。

为什么 wait() 要放在循环里?

因为被唤醒后不代表条件一定成立(可能是被误唤、条件还没满足):

while not 条件:
    condition.wait()

3. 底层原理

  • condition.wait() 会:

    • 释放底层锁;

    • 把当前线程加入等待队列;

    • 阻塞直到被 notify() 唤醒;

    • 再次尝试获取锁(被唤醒后不是立刻运行,要竞争锁);

  • condition.notify() 会:

    • 将等待队列中的线程移出,并让它尝试重新获取锁;

    • 不立即执行,要等持锁线程释放锁。

4. 实战案例:经典生产者消费者模型

import threading
import time

queue = []
MAX_SIZE = 5
condition = threading.Condition()

def producer():
    while True:
        with condition:
            while len(queue) >= MAX_SIZE:
                print("队列满,生产者等待")
                condition.wait()
            queue.append(1)
            print("生产者:生产了一个产品,现在库存", len(queue))
            condition.notify()
        time.sleep(0.5)

def consumer():
    while True:
        with condition:
            while not queue:
                print("队列空,消费者等待")
                condition.wait()
            queue.pop()
            print("消费者:消费了一个产品,现在库存", len(queue))
            condition.notify()
        time.sleep(1)

threading.Thread(target=producer).start()
threading.Thread(target=consumer).start()

Condition 与 Lock 的区别

比较点LockCondition
功能互斥访问资源等待条件满足与唤醒
是否能通知 无通知机制wait() + notify() 实现通信
场景单个共享资源同步多线程协调、等待/通知、状态依赖场景
是否必须配合锁本身就是锁必须配合底层锁(可传入或内部自建)

七、信号量(Semaphore

信号量(Semaphore)是一种用于控制“允许同时访问资源的线程数量”的同步机制。

可以理解为一个“计数器锁”:它允许最多 N 个线程同时访问某个临界资源。

举例说明:

  • 有 3 个数据库连接线程池,你最多只能允许 3 个线程同时使用。

  • 超过的线程必须等待——这时候信号量就能起作用。

1. 基本语法和使用方式

from threading import Semaphore, Thread
import time

# 最多允许 3 个线程同时执行
sem = Semaphore(3)

def task(name):
    print(f"{name} 想要进入")
    with sem:  # 等同于 acquire()/release()
        print(f"{name} 进入了")
        time.sleep(2)
    print(f"{name} 离开了")

for i in range(5):
    Thread(target=task, args=(f"线程{i}",)).start()

输出结果(简略):

线程0 想要进入
线程0 进入了
线程1 想要进入
线程1 进入了
线程2 想要进入
线程2 进入了
线程3 想要进入
线程4 想要进入
# 等 2 秒后...
线程0 离开了
线程3 进入了
线程1 离开了
线程4 进入了

2. 核心方法

方法说明
Semaphore(n)创建一个初始值为 n 的信号量
acquire()获取信号量(计数 -1,必要时阻塞)
release()释放信号量(计数 +1)

with sem: 会自动调用 acquire()release()

3. 信号量的原理

内部维护一个计数器:

  • 初始值为 n

  • 每次调用 acquire(),计数器减 1;

    • 如果计数器 < 0,线程进入阻塞等待;

  • 每次 release(),计数器加 1,唤醒等待线程。

4. 信号量 VS Lock 区别

比较点LockSemaphore(n)
控制线程数只允许 1 个线程允许最多 n 个线程
使用场景互斥访问资源资源池、线程配额控制等
阻塞机制有线程时阻塞等待超过 n 个时阻塞等待

5. BoundedSemaphore

是信号量的子类,防止 release() 超过最大值

from threading import BoundedSemaphore

# 最多允许 2 个线程
sem = BoundedSemaphore(2)

sem.acquire()
sem.release()
sem.release()  # 报错:释放次数多于获取次数

适合场景:确保资源归还次数不能多于借出次数(比如数据库连接池)。

6. 实战场景:多线程爬虫限并发

import threading
import time
import random

sem = threading.Semaphore(5)  # 最多 5 个并发线程

def download_page(url):
    with sem:
        print(f"下载 {url}")
        time.sleep(random.uniform(1, 3))
        print(f"完成 {url}")

urls = [f"http://example.com/page{i}" for i in range(20)]
threads = [threading.Thread(target=download_page, args=(url,)) for url in urls]

for t in threads: t.start()
for t in threads: t.join()
概念内容
信号量控制同时访问资源的线程数量
用法with Semaphore(n): 或手动 acquire() / release()
应用场景资源池管理、线程限流、连接控制
和 Lock 区别Lock 是一把“独占锁”,信号量是“计数锁”

八、GIL

GIL(Global Interpreter Lock)是 Python 解释器在执行字节码时使用的全局锁,同一时间只允许一个线程执行 Python 代码。

注意:它是 CPython 解释器(最常用的 Python 实现)特有的机制,其他如 PyPy、Jython 不一定有 GIL。

GIL 是为了简化 CPython 的内存管理而引入的。

  • Python 中很多操作依赖于 引用计数(ref count) 管理内存。

  • 如果多个线程同时修改一个对象的引用计数,没有加锁就会出错。

  • GIL 让 Python 在任意时刻只有一个线程执行解释器逻辑,这样可以避免加大量底层锁,提高安全性和实现效率。

GIL 是个互斥锁,具有以下特点:

  • 只要是运行 Python 字节码(解释器层),就必须先获取 GIL。

  • Python 会在每个线程中定时地释放 GIL(默认每隔一定“字节码次数”)。

  • 当 GIL 被释放后,其他线程才有机会获取它并执行。

  • GIL 不影响 IO 操作(比如网络请求、磁盘读写时会主动释放 GIL)。

1. GIL 对多线程的影响

CPU 密集型(CPU-bound)任务,指的是:

主要消耗 CPU 资源的任务,几乎不进行 I/O(例如磁盘、网络)操作。

常见的 CPU 密集型任务:

  • 数值计算(如大循环、大量加减乘除)

  • 图像处理(如像素遍历、滤波)

  • 加密解密、压缩解压

  • 人工智能模型推理

  • 编译、哈希计算

I/O 密集型(Input/Output-bound)任务指的是:

程序大部分时间花在等待外部设备(网络、磁盘、数据库等)响应,而不是 CPU 运算。

示例:

操作类型是否I/O密集
time.sleep()模拟
requests.get() 请求网页实际网络 I/O
open('file.txt').read()磁盘 I/O
数据库查询(如 MySQL)网络 I/O

对比:

情况表现
CPU 密集型任务多线程基本无效,效率反而更差
IO 密集型任务多线程有效,提高效率

CPU 密集型 示例(效果差)

import threading

def cpu_task():
    count = 0
    for _ in range(10**7):
        count += 1

threads = [threading.Thread(target=cpu_task) for _ in range(4)]

import time
start = time.time()
for t in threads: t.start()
for t in threads: t.join()
print("耗时:", time.time() - start)

多线程并不能加速,反而因为频繁切换 GIL 更慢!

IO 密集型 示例(效果好)

import threading
import time

def io_task():
    time.sleep(2)

threads = [threading.Thread(target=io_task) for _ in range(10)]
start = time.time()
for t in threads: t.start()
for t in threads: t.join()
print("耗时:", time.time() - start)

十个线程几乎同时 sleep,总耗时≈2秒,多线程是有效的。

2.  如何绕过 GIL 限制?

方案一:使用 multiprocessing(多进程)

from multiprocessing import Process

def cpu_task():
    count = 0
    for _ in range(10**7):
        count += 1

procs = [Process(target=cpu_task) for _ in range(4)]
for p in procs: p.start()
for p in procs: p.join()

每个进程有自己的 Python 解释器和内存空间,不受 GIL 限制,适合 CPU 密集型任务。

方案二:使用 C/C++ 写的扩展模块

  • NumPy、Pandas 内部用 C 实现了重计算部分,释放了 GIL,性能更强;

  • 可以用 cythoncffi 编写扩展模块绕过 GIL。

3. 常见误区

错误理解正确认知
GIL 会限制所有并发限制的是 线程的 CPU 并发,非 IO 并发
多线程一定加速 CPU 密集型会更慢;只有 IO 密集型适合多线程
Python 不能并发 多进程 + IO 并发可以实现高性能并发程序

4. 总结

内容说明
GIL 作用限制同一时间只能一个线程运行 Python 字节码
主要影响CPU 密集型任务多线程无效、甚至更慢
避免方法使用多进程、调用 C 扩展、用 asyncio 或其它解释器
多线程推荐场景网络爬虫、文件读写、数据库请求(IO 密集型)

九、asyncio(异步编程、协程)

asyncio 是 Python 内置的异步编程库,用于实现高并发的网络或 IO 应用(如爬虫、服务器、异步请求等),核心思想是:

非阻塞 + 协程(Coroutine)调度,不需要多线程也能并发执行。

名词含义
协程(Coroutine)可以被挂起和恢复的函数,async def 定义
事件循环(Event Loop)协调调度协程的机制
任务(Task)协程包装后可调度运行的对象
await暂停当前协程,等待另一个协程/异步操作完成

1. 最简单的 asyncio 示例

import asyncio

async def say_hello():
    print("你好")
    await asyncio.sleep(1)  # 模拟 IO 阻塞
    print("你好 again")

asyncio.run(say_hello())

运行结果:

你好
(等待1秒)
你好 again

特点:

  • async def 定义协程函数;

  • await 表示“挂起等待”,不会阻塞整个程序;

  • asyncio.run() 是启动入口。

2. 多个任务并发执行(核心优势)

import asyncio

async def task(name, delay):
    print(f"{name} 开始")
    await asyncio.sleep(delay)
    print(f"{name} 结束")

async def main():
    # 创建任务列表
    tasks = [
        asyncio.create_task(task("任务A", 2)),
        asyncio.create_task(task("任务B", 3)),
        asyncio.create_task(task("任务C", 1)),
    ]
    await asyncio.gather(*tasks)  # 并发运行

asyncio.run(main())

输出:

任务A 开始
任务B 开始
任务C 开始
(1秒后)任务C 结束
(2秒后)任务A 结束
(3秒后)任务B 结束

三秒内完成了三个任务,而不是串行等待!

3. asyncio 工作原理

原理图简述:

事件循环:
  |
  |---> 调度协程1(await I/O) → 挂起
  |---> 调度协程2(await I/O) → 挂起
  |---> 一旦某个 I/O 完成 → 恢复协程执行

它不会新建线程,而是用一个循环不断调度“哪些协程可以继续执行”。

4. 协程 VS 线程对比

对比点协程(asyncio线程(threading
创建开销 小(几 KB) 大(几 MB)
切换方式 协程调度器控制(用户态) 操作系统调度器(内核态)
上下文切换代价 小 大(涉及系统上下文、GIL)
并发能力 高(成千上万个协程) 中等(几十到几百个线程)
编码难度 稍高(需要 async/await 结构) 较简单(同步写法)
适合任务类型 IO 密集型 IO 密集型   CPU密集型“不推荐”
是否并行(多核) 否(单线程) 否(受 GIL 限制)

5.  常用 asyncio 组件

组件用途
asyncio.run()启动事件循环并运行协程
asyncio.create_task()创建任务(立即调度)
asyncio.gather()并发等待多个协程完成
asyncio.sleep()异步睡眠
asyncio.Queue异步队列,用于生产者消费者模型
asyncio.Semaphore限制并发量

6. 真实应用场景

异步爬虫

import asyncio
import aiohttp

async def fetch(session, url):
    async with session.get(url) as resp:
        print(f"{url} 状态码: {resp.status}")

async def main():
    urls = ["http://example.com"] * 5
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in urls]
        await asyncio.gather(*tasks)

asyncio.run(main())

使用 aiohttp + asyncio 可发起高效的并发网络请求,远胜多线程。

7. 与同步代码的对比

同步写法:

def foo():
    time.sleep(3)
    print("结束")

异步写法:

async def foo():
    await asyncio.sleep(3)
    print("结束")

await 后面必须跟异步函数,不能是普通的阻塞操作!


十、进程池与线程池

1. 进程池 multiprocessing.Pool

当你需要运行大量进程时,不建议手动创建 Process推荐用进程池统一管理

示例:

from multiprocessing import Pool
import os, time

def task(x):
    print(f"进程 {os.getpid()} 执行任务 {x}")
    time.sleep(1)
    return x * x

if __name__ == '__main__':
    with Pool(processes=4) as pool:
        results = pool.map(task, range(10))
    print("结果:", results)

输出(部分):

进程 12345 执行任务 0
进程 12346 执行任务 1
...
结果: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

优势:自动并发、自动回收资源、适合批量任务。

2. 线程池 concurrent.futures.ThreadPoolExecutor

适用于大量 IO 密集型任务。

from concurrent.futures import ThreadPoolExecutor
import time

def task(x):
    print(f"处理任务 {x}")
    time.sleep(1)
    return x * 2

with ThreadPoolExecutor(max_workers=5) as executor:
    results = list(executor.map(task, range(10)))
print("结果:", results)

3. 进程池 concurrent.futures.ProcessPoolExecutor

适用于 CPU 密集型任务,写法类似线程池。

from concurrent.futures import ProcessPoolExecutor
import time

def task(x):
    time.sleep(1)
    return x * x

with ProcessPoolExecutor(max_workers=4) as executor:
    results = list(executor.map(task, range(10)))
print("结果:", results)

4. 对比

项目进程池线程池
模块multiprocessing.PoolProcessPoolExecutorThreadPoolExecutor
并发粒度进程,独立解释器线程,共享解释器
内存高(不共享)低(共享)
GIL 影响
场景CPU 密集(加密、图像处理)IO 密集(网络、磁盘、爬虫)
任务提交方式mapsubmit
对比点concurrent.futures.ProcessPoolExecutormultiprocessing.Pool
所属模块concurrent.futures(Python 3.2+)multiprocessing(Python 2 就有)
编程风格 面向对象,现代异步接口(submit、future)函数式风格(apply、map)
返回结果Future 对象(可以异步获取)立即返回结果或阻塞等待
错误处理异常封装在 Future 中,易于捕获处理异常较麻烦
支持异步支持 asyncio 搭配使用(用 loop.run_in_executor不支持协程环境
可读性更清晰(使用 with 管理资源)稍老派,易出错
适合场景推荐用于现代异步并发、结构清晰的代码更适合简单场景或兼容旧代码
执行方式submit(fn, *args) 非阻塞提交任务map, apply, apply_async

5. submit + as_completed 进阶写法(线程池和进程池通用)

from concurrent.futures import ThreadPoolExecutor, as_completed

def task(x):
    time.sleep(1)
    return x * 2

with ThreadPoolExecutor(max_workers=5) as executor:
    futures = [executor.submit(task, i) for i in range(10)]
    for future in as_completed(futures):
        print("结果:", future.result())

适合逐个处理结果,而不是 map 那种批量等待。

6. 什么时候用哪种

任务类型推荐方式
网络请求 / 爬虫ThreadPoolExecutor
图像处理 / 计算ProcessPoolExecutor
批处理任务Pool.map()map_async()
高级调度控制手动管理 Process / Queue / Pipe

7. 总结

模块类型场景优点
threading线程IO 密集开销小、响应快
multiprocessing进程CPU 密集独立解释器、真正并行
ThreadPoolExecutor线程池网络、多任务自动线程管理,易用
ProcessPoolExecutor进程池多核计算并行执行,简单易用

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值