进程

进程是一个执行中的程序。每个进程都拥有自己的地址空间、内存、数据栈以及其他用于跟踪执行的辅助数据。在单核CPU系统中的多进程,内存中可以有许多程序,但在给定一个时刻只有一个程序在运行;就是说,可能这一秒在运行进程A,下一秒在运行进程B,虽然两者都在内存中,都没有真正同时运行。

线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。Python可以运行多线程,但和单核CPU多进程一样,在给定时刻只有一个线程会执行。

(1)利用Process来创建子进程

python
# -*- coding:utf-8 -*-
from multiprocessing import Process
from time import ctime, sleep

# 定义一个函数,用于多个进程执行
def job(n, sec):
    print("start job", n, "at:", ctime())
    sleep(sec)
    print("job", n, "done at:", ctime())


if __name__ == "__main__":
    p1 = Process(target=job, args=(1, 2)) #创建进程P1,实例化进程对象。target 指定要执行的函数,args 用元组(Tuple)形式向函数传递参数。
    p2 = Process(target=job, args=(2, 3)) #创建进程P2,实例化进程对象。target 指定要执行的函数,args 用元组(Tuple)形式向函数传递参数。
    p1.start() #异步启动进程。这行代码只负责向操作系统发出“启动进程”的指令,不会等待子进程执行完毕,主进程会立刻向下执行。
    p2.start() #异步启动进程。这行代码只负责向操作系统发出“启动进程”的指令,不会等待子进程执行完毕,主进程会立刻向下执行。

    print("main job") #主进程继续,打印信息
    
    p1.join()  #告诉主进程:“你必须在这里等着,直到 p1 进程完全运行结束,才能向下走。”
    print("main wait")  # 由于p1.join()阻塞,这里主进程会在2秒后打印,就是p1进程执行完后主进程才会往后执行
    p2.join() #告诉主进程:“你必须在这里等着,直到 p2 进程完全运行结束,才能向下走。”

    print("finished") #因为有 join() 的保护,这行代码百分之百是全程序最后一行打印的内容。它保证了主进程在所有子进程收尾后才退出。

执行结果
main job
start job 1 at: Fri Jul 31 11:32:45 2026start job
 2 at: Fri Jul 31 11:32:45 2026
job 1 done at: Fri Jul 31 11:32:47 2026
main wait
job 2 done at: Fri Jul 31 11:32:48 2026
finished

(2)利用进程池

python
# -*- coding:utf-8 -*-
from multiprocessing import Pool
from time import ctime, sleep
import time

def job(n, sec):
    print("start job", n, "at:", ctime())
    sleep(sec)
    print("job", n, "done at:", ctime())


if __name__ == "__main__":
    pool = Pool(processes=3)  #进程池容量3
    START_TIME = time.time()
    for i, j in zip([1, 2, 3, 4], [4, 3, 2, 1]):
        # 维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去
        pool.apply_async(job, args=(i, j))
    pool.close()
    # 调用join之前,先调用close函数,否则会出错。执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束
    pool.join()
    end_offset = time.time() - START_TIME
    print(f"任务耗时: {end_offset:.4f} 秒")
    print("finished")

执行结果
    start job 1 at: Fri Jul 31 11:52:00 2026
    start job 2 at: Fri Jul 31 11:52:00 2026
    start job 3 at: Fri Jul 31 11:52:00 2026
    job 3 done at: Fri Jul 31 11:52:02 2026
    start job 4 at: Fri Jul 31 11:52:02 2026
    job 2 done at: Fri Jul 31 11:52:03 2026
    job 4 done at: Fri Jul 31 11:52:03 2026
    job 1 done at: Fri Jul 31 11:52:04 2026
    任务耗时: 4.0703 秒
    finished

任务实际耗时4秒,表面上任务池只有3个,任务有4个,有一个需要等待队列,但是任务3只耗时2秒,2秒后开始执行任务4,任务4只耗时1秒,任务4完成后任务1仍在执行。

(3)利用ProcessPoolExecutor

从Python3.2开始,标准库 concurrent.futures 模块提供了ProcessPoolExecutor (进程池)供我们使用

python
from concurrent.futures import ProcessPoolExecutor
from time import ctime, sleep
import time


def job(n, sec):
    print("start job", n, "at:", ctime())
    sleep(sec)
    print("job", n, "done at:", ctime())


if __name__ == '__main__':
    START_TIME = time.time()
    with ProcessPoolExecutor(max_workers=3) as executor:
        all_jobs = [executor.submit(job, i, j) for i, j in zip([1, 2, 3, 4], [4, 3, 2, 1])]
    end_offset = time.time() - START_TIME
    print(f"任务耗时: {end_offset:.4f} 秒")
    print("finished")

执行结果
start job 1 at: Fri Jul 31 19:36:51 2026
start job 2 at: Fri Jul 31 19:36:51 2026
start job 3 at: Fri Jul 31 19:36:51 2026
job 3 done at: Fri Jul 31 19:36:53 2026
start job 4 at: Fri Jul 31 19:36:53 2026
job 2 done at: Fri Jul 31 19:36:54 2026
job 4 done at: Fri Jul 31 19:36:54 2026
job 1 done at: Fri Jul 31 19:36:55 2026
任务耗时: 4.0911 秒
finished

核心总结

  • 现代标准开发、需要获取异步执行状态、处理复杂任务流 ➡️ 推荐首选 concurrent.futures(更现代安全)
  • 海量重复计算(如矩阵、数学运算、数据批量处理) ➡️ 选 multiprocessing.Pool
  • 只需要一两个长驻后台的监听进程,想手动控制其开关 ➡️ 选 multiprocessing.Process。

线程

(1)利用Thread创建子线程

python
import threading
from time import ctime, sleep
import time


def job(n, sec):
    print("start job", n, "at:", ctime())
    sleep(sec)
    print("job", n, "done at:", ctime())


if __name__ == '__main__':
    threads = []
    for i, j in zip([1, 2, 3, 4], [4, 3, 2, 1]):
        t = threading.Thread(target=job, args=(i, j))
        threads.append(t)
    start_time = time.time()
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    end_offset = time.time() - start_time
    print(f"任务耗时: {end_offset:.4f} 秒")
    print("finished")

当所有线程都分配完成之后,通过调用每个线程的start()方法让它们开始执行,而不是在这之前就会执行。join()方法将等待线程结束,或者在提供了超时时间的情况下,达到超时时间。相比于管理一组锁(分配、获取、释放、检查锁状态等)而言,使用join()方法要比等待锁释放的无限循环更加清晰(这也是这种锁又称为自旋锁的原因)。 对于 join()方法而言,其另一个重要方面是其实它根本不需要调用。一旦线程启动,它们就会一直执行,直到给定的函数完成后退出。如果主线程还有其他事情要去做,而不是等待这些线程完成(例如其他处理或者等待新的客户端请求),就可以不调用join()。join()方法只有在你需要等待线程完成的时候才是有用的。

执行结果
    start job 1 start job start job 3at: Fri Jul 31 19:54:31 2026
    start job 2  at\:at:  Fri Jul 31 19:54:31 2026Fri Jul 31 19:54:31 2026

    4 at: Fri Jul 31 19:54:31 2026
    job 4 done at: Fri Jul 31 19:54:32 2026
    job 3 done at: Fri Jul 31 19:54:33 2026
    job 2 done at: Fri Jul 31 19:54:34 2026
    job 1 done at: Fri Jul 31 19:54:35 2026
    任务耗时: 4.0053 秒
    finished

这里出现神奇的效果,信息打印似乎是乱的,这是一个非常经典的多线程并发控制与标准输出(I/O 缓存)竞争导致的问题。这段代码之所以打印出来是“乱的”(错位、空行、或者任务编号和时间混在一起),主要是因为以下两个核心原因:

  • 原因 1:print 函数不是线程安全的(根本原因) Python 的 print() 函数虽然看起来只有一行,但在底层其实执行了两个步骤:

    • 打印你传给它的内容(比如 "start job 1 at: ...")。
    • 打印末尾的换行符\n。 在多线程环境下,四个线程是**同时(并发)**向操作系统的同一个控制台(Terminal)发送打印指令的。 当线程 1 刚打印完内容,还没来得及打印换行符 \n 时,操作系统突然把 CPU 切换给了线程 2。线程 2 就会把自己的内容直接接在线程 1 的屁股后面,随后两个换行符连续喷出,就会导致整行错位或者出现莫名其妙的空行。
  • 原因 2:time.sleep 让出了GIL,导致纯粹的并发 你可能会想:“Python 不是有GIL(全局解释器锁)吗?为什么还会乱?”

    • GIL 只能保证 Python 内部字节码 同一时刻只有一个在执行。
    • 但是,当线程遇到 time.sleep() 或者进行 print()(I/O 操作)时,线程会主动把 GIL 锁释放掉,让给其他线程。
    • 代码中,t.start() 几乎是瞬间同时启动了 4 个线程。它们一启动,就同时往控制台挤,这就演变成了纯粹的控制台抢占。

修复"打印混乱"

方案 A:使用“线程锁(Lock)”保护打印(推荐)
python
import threading
from time import ctime, sleep
import time

# 创建一把全局线程锁
print_lock = threading.Lock()

def job(n, sec):
    # 使用 lock 保护启动打印
    with print_lock:
        print("start job", n, "at:", ctime())
    sleep(sec)
    # 使用 lock 保护启动打印
    with print_lock:
        print("job", n, "done at:", ctime())


if __name__ == '__main__':
    threads = []
    for i, j in zip([1, 2, 3, 4], [4, 3, 2, 1]):
        t = threading.Thread(target=job, args=(i, j))
        threads.append(t)
    start_time = time.time()
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    end_offset = time.time() - start_time
    print(f"任务耗时: {end_offset:.4f} 秒")
    print("finished")

执行结果
python
start job 1 at: Fri Jul 31 20:17:49 2026
start job 2 at: Fri Jul 31 20:17:49 2026
start job 3 at: Fri Jul 31 20:17:49 2026
start job 4 at: Fri Jul 31 20:17:49 2026
job 4 done at: Fri Jul 31 20:17:50 2026
job 3 done at: Fri Jul 31 20:17:51 2026
job 2 done at: Fri Jul 31 20:17:52 2026
job 1 done at: Fri Jul 31 20:17:53 2026
任务耗时: 4.0056 秒
finished
方案 B:改用单次原子操作(临时小技巧)

可以把要打印的零散参数先用 f-string 拼接成一个完整的单字符串。在很多操作系统中,打印单个字符串是一个原子操作,被切断的概率会大大降低:

python
import threading
from time import ctime, sleep
import time


def job(n, sec):
    print(f"start job {n} at: {ctime()}")
    sleep(sec)
    print(f"job {n} done at: {ctime()}")


if __name__ == '__main__':
    threads = []
    for i, j in zip([1, 2, 3, 4], [4, 3, 2, 1]):
        t = threading.Thread(target=job, args=(i, j))
        threads.append(t)
    start_time = time.time()
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    end_offset = time.time() - start_time
    print(f"任务耗时: {end_offset:.4f} 秒")
    print("finished")

执行结果
python
start job 1 at: Fri Jul 31 20:21:55 2026
start job 2 at: Fri Jul 31 20:21:55 2026
start job 3 at: Fri Jul 31 20:21:55 2026
start job 4 at: Fri Jul 31 20:21:55 2026
job 4 done at: Fri Jul 31 20:21:56 2026
job 3 done at: Fri Jul 31 20:21:57 2026
job 2 done at: Fri Jul 31 20:21:58 2026
job 1 done at: Fri Jul 31 20:21:59 2026
任务耗时: 4.0054 秒
finished

似乎多进程不会出现打印混乱? 多进程(multiprocessing)之所以看起来不会像多线程那样出现严重的打印错乱,主要由两层深层原因决定:

  • 原因 1:操作系统底层的「原子缓冲写入」机制 这是最核心的原因。
    • 多线程:所有线程共享同一个进程的输出缓冲区。它们共用同一个 C 语言级别的 stdout 文件句柄。当多个线程同时调用 print 时,字符在内存缓冲区里就已经混在一起了,随后被一起刷新(Flush)到屏幕上,导致内容直接错位。
    • 多进程:每一个进程都是独立的程序,拥有完全独立的内存和独立的标准输出缓冲区。当子进程执行 print() 时,它是把整行字符串先写进自己的缓冲区里。当缓冲区满了或者遇到换行符时,系统内核(Kernel)会调用底层的 write() 系统调用。
    • 关键点:在现代操作系统(Linux/macOS/Windows)中,对终端(Terminal)进行单次 write() 写入时,通常具有行级别的原子性(Line-buffered Atomicity)。这意味着即使两个进程同时发送数据,操作系统也会排队,先完整地把进程 A 的一行吐出来,再完整地吐出进程 B 的一行,从而极大地降低了行内文字互相截断、混杂的概率。
  • 原因 2:进程创建的「时间差」天然错开了并发 代码中,看起来它们是“同时”启动的,但实际上:
    • 多线程 的创建极其轻量(只需要开辟一点栈空间),t.start() 之间的时间差只有几微秒,4 个线程几乎是真正瞬间卡在同一个时间点去抢占控制台。
    • 多进程 的创建非常沉重(操作系统需要分配独立的内存空间、复制或创建新的进程上下文、加载 Python 解释器)。
      • 即使你写了循环 p.start(),进程 1 和进程 2 的实际启动时间往往相差了几毫秒甚至十几毫秒。
      • 这一点点微小的系统开销时间差,在计算机眼里已经长得像“世纪”一样了。由于它们启动本身就有先后,第一句 start job 打印自然就被物理上错开了。

注意:多进程并不是「绝对不会乱」 虽然多进程不会发生“一行字中间被卡断”的惨剧,但它依然无法控制整行的先后顺序。 如果你打印的数据非常庞大(例如每个进程一口气 print 几万字),或者控制台的性能很差,多进程之间依然会出现整行顺序颠倒的情况。为了保证 100% 的绝对安全,无论是多进程还是多线程,在遇到多产出(多并发写入)时,标准做法依然是: 1.多线程:使用 threading.Lock()。 2.多进程:使用 multiprocessing.Lock(),或者将所有子进程的打印信息通过 Queue(队列)统一发送给主进程,由主进程单线程统一负责打印(这是工业级开发最推荐的做法)。

多线程的乱:是“字符级”的粉碎性错位 多进程的乱:是“整行级”的乱序

利用Thread创建子线程进阶

我们可以创建一个类继承threading.Thead,让这个类更加通用,而不只是针对loop()函数,如果我们有别的函数也可以用这个类来使用多线程。我们需要覆写Thread的__init__()和run()方法,或者调用父类的__init__()然后覆写run()方法。

Python官方文档:https://docs.python.org/3/library/threading.html#thread-objects

python
import threading
from time import ctime, sleep
import time


class MyThread(threading.Thread):
    def __init__(self, func, args, name=""):
        threading.Thread.__init__(self)
        self.name = name
        self.func = func
        self.args = args

    def run(self):
        self.func(*self.args)


def job(n, sec):
    print(f"start job {n} at: {ctime()}")
    sleep(sec)
    print(f"job {n} done at: {ctime()}")


if __name__ == '__main__':
    threads = []
    for i, j in zip([1, 2, 3, 4], [4, 3, 2, 1]):
        t = MyThread(job, args=(i, j), name=job.__name__)
        threads.append(t)
    start_time = time.time()
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    end_offset = time.time() - start_time
    print(f"任务耗时: {end_offset:.4f} 秒")
    print("finished")

(2)利用线程池ThreadPool

python
from multiprocessing.dummy import Pool as ThreadPool
from time import ctime, sleep
import time

def job(n, sec):
    print(f"start job {n} at: {ctime()}")
    sleep(sec)
    print(f"job {n} done at: {ctime()}")


if __name__ == "__main__":
    pool = ThreadPool(processes=3)
    START_TIME = time.time()
    for i, j in zip([1, 2, 3, 4], [4, 3, 2, 1]):
        pool.apply_async(job, args=(i, j))
    pool.close()
    pool.join()
    end_offset = time.time() - START_TIME
    print(f"任务耗时: {end_offset:.4f} 秒")
    print("finished")

执行结果
start job 1 at: Sat Aug  1 02:11:23 2026
start job 2 at: Sat Aug  1 02:11:23 2026
start job 3 at: Sat Aug  1 02:11:23 2026
job 3 done at: Sat Aug  1 02:11:25 2026
start job 4 at: Sat Aug  1 02:11:25 2026
job 2 done at: Sat Aug  1 02:11:26 2026
job 4 done at: Sat Aug  1 02:11:26 2026
job 1 done at: Sat Aug  1 02:11:27 2026
任务耗时: 4.0069 秒
finished

(3)利用ThreadPoolExecutor

从Python3.2开始,标准库 concurrent.futures 模块提供了ThreadPoolExecutor (线程池)供我们使用

python
from concurrent.futures import ThreadPoolExecutor
from time import ctime, sleep
import time


def job(n, sec):
    print(f"start job {n} at: {ctime()}")
    sleep(sec)
    print(f"job {n} done at: {ctime()}")


if __name__ == '__main__':
    START_TIME = time.time()
    with ThreadPoolExecutor(max_workers=3) as executor:
        all_jobs = [executor.submit(job, i, j) for i, j in zip([1, 2, 3, 4], [4, 3, 2, 1])]
    end_offset = time.time() - START_TIME
    print(f"任务耗时: {end_offset:.4f} 秒")
    print("finished")

执行结果
start job 1 at: Sat Aug  1 02:09:44 2026
start job 2 at: Sat Aug  1 02:09:44 2026
start job 3 at: Sat Aug  1 02:09:44 2026
job 3 done at: Sat Aug  1 02:09:46 2026
start job 4 at: Sat Aug  1 02:09:46 2026
job 2 done at: Sat Aug  1 02:09:47 2026
job 4 done at: Sat Aug  1 02:09:47 2026
job 1 done at: Sat Aug  1 02:09:48 2026
任务耗时: 4.0054 秒
finished

协程

协程,英文Coroutines,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。

这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

引入

带有yield的函数不再是普通函数,而是生成器。send可以把一个函数的结果传给另外一个函数,以此实现单线程内程序之间的切换。

python
# -*- coding:utf-8 -*-
def consumer():
    r = ''
    while True:
        print("start...")
        n = yield r
        print("Consuming  running ....")
        if not n:
            return
        print('[CONSUMER] Consuming %s...' % n)
        r = '200 OK'


def produce(c):
    r = c.send(None)  # 等价于next(c)
    print('[PRODUCER] Consumer return: %s' % r)
    print("start consumer...")
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        r = c.send(n)
        print('[PRODUCER] Consumer return: %s' % r)
    c.close()


c = consumer()
produce(c)

执行结果
start...
[PRODUCER] Consumer return: 
start consumer...
[PRODUCER] Producing 1...
Consuming  running ....
[CONSUMER] Consuming 1...
start...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
Consuming  running ....
[CONSUMER] Consuming 2...
start...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
Consuming  running ....
[CONSUMER] Consuming 3...
start...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
Consuming  running ....
[CONSUMER] Consuming 4...
start...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
Consuming  running ....
[CONSUMER] Consuming 5...
start...
[PRODUCER] Consumer return: 200 OK

这里,produce先用 c.send(None) 启动生成器,consumer() 执行到 yield r 便停下来,并将r返回 给调用它的函数(比如next()或send());这时候consumer()被挂起,produce继续执行,当运行到 r=c.send(n) 时又让consumer()执行 ;此时consumer()将r赋值给n,并继续往下运行,执行print()函数,并将 '200 OK' 赋值给 r ;之后进入下一个while循环,又到了 yield r ,这时就跟前面一样了,停下了将r返回给调用它的函数,这时produce()里 r=c.send(n);不断重复上面,直到循环结束,c.close()关闭生成器。

详细拆解 n = yield r 这行代码其实做了三件事,按顺序发生:

  • 1.产出值(Output):
    • 表达式 yield r 首先计算 r 的值,并将其‌返回‌给调用者(即 send() 或 next() 的返回值)。
    • 此时函数暂停。
  • 2.挂起等待(Suspend):
    • 程序停在这一行,等待外部唤醒。
    • 只有外部执行调用 send 或 next才会继续向下执行
  • 3.接收值(Input):
    • 当外部调用 c.send(value) 时,value 会被传入。
    • 这个 value 被赋值给等号左边的变量 n。
    • 函数从这一行继续向下执行。

异步编程

(1)asyncio

asyncio 是 Python 用于编写并发代码的标准库,它利用 async/await 语法,是现代 Python(如 FastAPI、Tornado 等框架)实现高性能、高并发网络服务的核心基石。为了彻底理解它,我们需要从它的底层核心概念、运行机制、核心语法到实际避坑指南进行全面拆解。

核心思想:为什么需要 asyncio?

在理解 asyncio 之前,必须先理清它与多线程/多进程的区别:

  • 多进程(Multiprocessing):利用多核 CPU,真正并行。适合 CPU 密集型任务(如科学计算),但进程切换代价昂贵,内存独立。
  • 多线程(Threading):由于 GIL 的存在,无法利用多核。适合 I/O 密集型任务,但线程切换需要操作系统介入(上下文切换),且需要考虑死锁、线程安全等问题。
  • 异步 I/O(asyncio):单进程、单线程。它通过一个叫事件循环(Event Loop)的调度器,在任务遇到 I/O 阻塞(如等网络响应、读写文件)时,主动交出 CPU 控制权,让 CPU 去执行其他任务。它几乎没有上下文切换开销,一台机器可以轻松支撑上万个并发连接。

五大核心概念

必须搞懂以下五个名词:

  • 1.事件循环(Event Loop): 整个异步程序的“引擎”或“大管家”。它是一个无限循环,负责监听事件、管理并调度所有异步任务。
  • 2.协程(Coroutine): 用 async def 定义的函数。调用它不会立刻执行,而是返回一个协程对象。必须把它扔进事件循环或者用 await 才能触发执行。
  • 3.可等待对象(Awaitable): 可以在 await 关键字后面使用的对象。主要有三类:Coroutine(协程)、Task(任务) 和 Future(未来对象)。
  • 4.任务(Task): 对协程的进一步封装。将协程打包成 Task 后,该任务会被立刻自动注册到事件循环中并开始在后台排队运行。
  • 5.Future: 底层的异步对象,代表一个“未来才会产生的结果”。通常不需要手动创建,它是 Task 的基类。

核心语法与代码

asyncio.coroutine

@asyncio.coroutine 是 Python 3.4 时代引入的旧版协程装饰器。由于 Python 发展,它在 Python 3.8 中被弃用,并在 Python 3.11 中已被完全移除。现代 Python 编程应统一使用更为直观的 async def 和 await 关键字。

示例
python
import asyncio

# 警告:以下代码在 Python 3.11+ 环境下运行会报错
@asyncio.coroutine
def compute_square(x):
    print(f"开始计算 {x} 的平方...")
    # yield from 相当于现在的 await,用于挂起当前协程并等待耗时 I/O 
    yield from asyncio.sleep(1)
    result = x * x
    print(f"计算完成: {result}")
    return result

@asyncio.coroutine
def main():
    print("主程序启动")
    # 等待 compute_square 执行完毕
    res = yield from compute_square(5)
    print(f"最终结果: {res}")

# 获取事件循环并运行
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

现代 async def / await 语法示例(标准推荐)这是当前最安全、可读性最好的方式,能无缝运行在所有主流 Python 3 版本中。

python
import asyncio

async def hello():
    print("Hello world!")
    # 异步调用asyncio.sleep(1):
    await asyncio.sleep(1)
    print("Hello again!")

asyncio.run(hello())

async把一个函数变成coroutine类型,然后,我们就把这个async函数扔到asyncio.run()中执行。执行结果如下:

Hello world!
(等待约1秒)
Hello again!

hello()会首先打印出Hello world!,然后,await语法可以让我们方便地调用另一个async函数。由于asyncio.sleep()也是一个async函数,所以线程不会等待asyncio.sleep(),而是直接中断并执行下一个消息循环。当asyncio.sleep()返回时,就接着执行下一行语句。

把asyncio.sleep(1)看成是一个耗时1秒的IO操作,在此期间,主线程并未等待,而是去执行EventLoop中其他可以执行的async函数了,因此可以实现并发执行。

上述hello()还没有看出并发执行的特点,我们改写一下,让两个hello()同时并发执行:

python
import asyncio
import threading

# 传入name参数:
async def hello(name):
    # 打印name和当前线程:
    print("Hello %s! (%s)" % (name, threading.current_thread))
    # 异步调用asyncio.sleep(1):
    await asyncio.sleep(1)
    print("Hello %s again! (%s)" % (name, threading.current_thread))
    return name

#用asyncio.gather()同时调度多个async函数:
async def main():
    L = await asyncio.gather(hello("Bob"), hello("Alice"))
    print(L)

asyncio.run(main())
执行结果
Hello Bob! (<function current_thread at 0x105038400>)
Hello Alice! (<function current_thread at 0x105038400>)
Hello Bob again! (<function current_thread at 0x105038400>)
Hello Alice again! (<function current_thread at 0x105038400>)
['Bob', 'Alice']

从结果可知,用asyncio.run()执行async函数,所有函数均由同一个线程执行。两个hello()是并发执行的,并且可以拿到async函数执行的结果(即return的返回值)。

如果把asyncio.sleep()换成真正的IO操作,则多个并发的IO操作实际上可以由一个线程并发执行。 我们用asyncio的异步网络连接来获取sina、sohu和163的网站首页:

python
import asyncio

async def wget(host):
    print(f"wget {host}...")
    # 连接80端口:
    reader, writer = await asyncio.open_connection(host, 80)
    # 发送HTTP请求:
    header = f"GET / HTTP/1.0\r\nHost: {host}\r\n\r\n"
    writer.write(header.encode("utf-8"))
    await writer.drain()

    # 读取HTTP响应:
    while True:
        line = await reader.readline()
        if line == b"\r\n":
            break
        print("%s header > %s" % (host, line.decode("utf-8").rstrip()))
    # Ignore the body, close the socket
    writer.close()
    await writer.wait_closed()
    print(f"Done {host}.")

async def main():
    await asyncio.gather(wget("www.sina.com.cn"), wget("www.sohu.com"), wget("www.163.com"))

asyncio.run(main())

执行结果
wget www.sina.com.cn...
wget www.sohu.com...
wget www.163.com...
www.sina.com.cn header > HTTP/1.1 302 Found
www.sina.com.cn header > Server: Tengine
www.sina.com.cn header > Date: Fri, 31 Jul 2026 21:20:24 GMT
www.sina.com.cn header > Content-Type: text/html
www.sina.com.cn header > Content-Length: 242
www.sina.com.cn header > Connection: close
www.sina.com.cn header > Location: https://www.sina.com.cn/
www.sina.com.cn header > X-DSL-CHECK: 5
www.sina.com.cn header > X-Via-CDN: f=aliyun,s=cache2.cn9393,c=163.125.129.201;
www.sina.com.cn header > Via: cache2.cn9393[,0]
www.sina.com.cn header > Timing-Allow-Origin: *
www.sina.com.cn header > EagleId: 705f081617855328246488065e
Done www.sina.com.cn.
www.sohu.com header > HTTP/1.1 302 Found
www.sohu.com header > Location: https://www.sohu.com/
www.sohu.com header > Content-Length: 0
www.sohu.com header > X-NWS-LOG-UUID: 1809137455161019864
www.sohu.com header > Connection: close
www.sohu.com header > Server: D0
www.sohu.com header > Date: Fri, 31 Jul 2026 21:20:24 GMT
www.sohu.com header > X-Cache-Lookup: Return Directly
Done www.sohu.com.
www.163.com header > HTTP/1.1 403 Forbidden
www.163.com header > Server: nginx
www.163.com header > Date: Fri, 31 Jul 2026 21:20:24 GMT
www.163.com header > Content-Type: text/html
www.163.com header > Content-Length: 146
www.163.com header > Connection: close
www.163.com header > X-Cache-Status: MISS
www.163.com header > X-Error-Info: acls.access
www.163.com header > Cache-Control: no-cache,no-store,private
www.163.com header > cdn-user-ip: 163.125.129.201
www.163.com header > cdn-ip: 36.250.238.35
www.163.com header > cdn-source: bdcloud
www.163.com header > X-Cache-Remote: MISS
Done www.163.com.
小结

asyncio提供了完善的异步IO支持,用asyncio.run()调度一个coroutine;

在一个async函数内部,通过await可以调用另一个async函数,这个调用看起来是串行执行的,但实际上是由asyncio内部的消息循环控制;

在一个async函数内部,通过await asyncio.gather()可以并发执行若干个async函数。

基础:声明与运行协程
  • 1.create_task一般形式创建
python
import asyncio
import warnings

# 激活废弃警告,让 Python 3.12+ 显式打印出 get_event_loop() 的警告信息
warnings.simplefilter('default', DeprecationWarning)

async def background_job(name, duration):
    """一个简单的异步任务"""
    print(f"[{name}] 任务启动...")
    await asyncio.sleep(duration)
    print(f"[{name}] 任务在 {duration} 秒后成功完成!")
    return f"{name} 的数据"


# =====================================================================
# 方案 1:现代标准高层级模式(asyncio.run + get_running_loop)
# =====================================================================
async def modern_main():
    print("\n--- 开始方案 1:现代标准模式 ---")
    
    # 1. 在异步上下文内,安全地获取当前正在运行的 loop
    loop = asyncio.get_running_loop()
    print(f"成功获取运行中的 loop: {loop}")
    
    # 2. 使用 loop 形式创建并启动任务
    task1 = loop.create_task(background_job("Modern-Task-1", 1))
    
    # 3. 使用通常形式创建任务(内部也是自动找 running_loop)
    task2 = asyncio.create_task(background_job("Modern-Task-2", 1.5))
    
    # 并发等待它们完成
    results = await asyncio.gather(task1, task2)
    print(f"方案 1 完成,收到结果: {results}")


# =====================================================================
# 方案 2:底层手动管理模式(new_event_loop + set_event_loop)
# =====================================================================
def manual_loop_main():
    print("\n--- 开始方案 2:底层手动管理模式 ---")
    
    # 1. 显式创建一个全新的事件循环(替代旧的 get_event_loop 自动创建行为)
    custom_loop = asyncio.new_event_loop()
    
    # 2. 将该循环显式绑定到当前线程上下文中
    asyncio.set_event_loop(custom_loop)
    
    try:
        # 3. 使用这个 loop 形式创建并调度任务
        # 注意:这里尚未开始运行 loop,任务只是被放进了队列
        task = custom_loop.create_task(background_job("Manual-Task", 1))
        
        # 4. 显式运行该循环,直到指定的任务(或主逻辑)完成
        result = custom_loop.run_until_complete(task)
        print(f"方案 2 完成,收到结果: {result}")
        
    finally:
        # 5. 必须手动关闭循环以释放底层资源
        custom_loop.close()
        print("手动创建的 loop 已安全关闭。")


# =====================================================================
# 方案 3:为什么不推荐旧的 get_event_loop()?(演示警告)
# =====================================================================
def legacy_loop_demo():
    print("\n--- 演示方案 3:旧的 get_event_loop() 行为 ---")
    print("注意观察下方是否会触发 DeprecationWarning 警告:")
    
    try:
        # 在没有运行中的事件循环时调用此方法:
        # Python 3.10+ 会警告,Python 3.12+ 明确标记为废弃,未来版本将直接报错
        legacy_loop = asyncio.get_event_loop() 
        print(f"虽然拿到了隐式创建的 loop: {legacy_loop}")
        
        # 运行它以防止资源泄漏
        legacy_loop.run_until_complete(background_job("Legacy-Task", 0.5))
        legacy_loop.close()
    except Exception as e:
        print(f"运行失败(如果在某些严格的异步环境中会直接报错): {e}")


# =====================================================================
# 主程序入口
# =====================================================================
if __name__ == "__main__":
    # 执行方案 1:这是 95% 的 Python 异步应用应该使用的标准入口
    asyncio.run(modern_main())
    
    # 执行方案 2:这是编写底层网络框架或多线程服务端时使用的控制入口
    manual_loop_main()
    
    # 执行方案 3:演示旧方法的弊端
    legacy_loop_demo()

  • 2.自动创建事件循环并运行协
python
import asyncio
import time

# 1. 声明一个协程函数
async def say_hello():
    print("Hello")
    # 注意:在异步代码中,绝对不能使用标准的 time.sleep()!
    # 因为 time.sleep 是同步阻塞的,会卡死整个单线程事件循环。
    await asyncio.sleep(1)
    print("World")

if __name__ == "__main__":
    # 2. asyncio.run 是高层入口,负责自动创建事件循环并运行协程
    asyncio.run(say_hello())
进阶:如何真正实现“并发”执行?

如果只是写 await job1() 紧接着 await job2(),它们依然是串行(一个接一个)运行的。要实现真正的并发,必须使用 asyncio.gather 或将其封装为 Task。

python
import asyncio
import time


async def fetch_data(id, delay):
    print(f"【📥 开始】下载任务 {id}...")
    await asyncio.sleep(delay)  # 模拟网络请求耗时
    print(f"【✅ 完成】任务 {id} 结束")
    return f"数据 {id}"


async def main():
    start = time.time()

    # 方式 A:使用 asyncio.gather 并发运行多个协程
    # gather 会把这些协程自动打包成 Task 并发执行,并等待所有任务完成
    results = await asyncio.gather(
        fetch_data(1, 3),
        fetch_data(2, 2),
        fetch_data(3, 1)
    )

    print("-" * 30)
    print(f"收到所有数据: {results}")
    print(f"总耗时: {time.time() - start:.2f} 秒")


if __name__ == "__main__":
    asyncio.run(main())

执行结果
【📥 开始】下载任务 1...
【📥 开始】下载任务 2...
【📥 开始】下载任务 3...
【✅ 完成】任务 3 结束
【✅ 完成】任务 2 结束
【✅ 完成】任务 1 结束
------------------------------
收到所有数据: ['数据 1', '数据 2', '数据 3']
总耗时: 3.00 秒

Python 推崇的 Task 机制(Python 3.11+)

在早期的 Python 中,我们常用 asyncio.create_task() 来创建后台任务。为了防止任务被垃圾回收机制(GC)中途销毁,现代 Python 更推荐使用 asyncio.TaskGroup(上下文管理器模式,类似于 Golang 的 errgroup):

python
import asyncio
import time


async def fetch_data(id, delay):
    print(f"【📥 开始】下载任务 {id}...")
    await asyncio.sleep(delay)  # 模拟网络请求耗时
    print(f"【✅ 完成】任务 {id} 结束")
    return f"数据 {id}"

async def main():
    # TaskGroup 会在 with 块结束时,自动确保里面的所有任务都执行完毕
    async with asyncio.TaskGroup() as tg:
        # tg.create_task 会让任务立刻在后台异步跑起来
        task1 = tg.create_task(fetch_data(1, 2))
        task2 = tg.create_task(fetch_data(2, 1))

    # 当走出 with 块后,可以通过 .result() 直接拿到结果
    print(task1.result(), task2.result())

if __name__ == "__main__":
    asyncio.run(main())

(2)greenlet、gevent、猴子补丁

Greenlet、Gevent 和 猴子补丁(Monkey Patching)代表了 Python 异步编程的另一条技术路线:微线程(Micro-threads)。它们与 asyncio 的核心区别在于自动调度与兼容性。

核心概念解析

1.Greenlet:底层原语
  • 定义:一个用 C 编写的轻量级协程库,提供了比原生生成器更灵活的“任意跳转”能力。
  • 特点:它没有内置的事件循环或调度器,需要手动切换(switch)。它是 Gevent 的基础。
  • 地位:相当于协程界的“汇编语言”,极少直接使用,通常作为其他库的底层支撑。
2.Gevent:基于 Greenlet 的框架
  • 定义:一个并发网络库,通过封装 Greenlet 实现了‌协作式多任务‌。
  • 核心机制:提供了一个事件循环,当遇到 I/O 操作(如网络请求、文件读写)时,自动挂起当前 Greenlet 并切换到另一个,从而实现高并发。
  • 优势:代码写法完全是‌同步风格‌,无需使用 async/await,降低了开发心智负担。
3.猴子补丁(Monkey Patching):兼容同步代码的关键
  • 定义:在运行时动态替换标准库中的阻塞模块(如 socket, threading, ssl)为 Gevent 提供的非阻塞版本。
  • 作用:让原本为同步阻塞设计的第三方库或标准库代码,无需修改源码即可在 Gevent 环境下实现异步并发。
  • 用法:通常在程序入口处调用 from gevent import monkey; monkey.patch_all()。

示例代码

使用greenlet完成协程任务 环境安装包 pip install greenlet

python
import time
from greenlet import greenlet


def a():  # 任务A
    for i in range(5):
        print('A' + str(i))
        gb.switch()
        time.sleep(0.1)


def b():  # 任务B
    for i in range(5):
        print('B' + str(i))
        gc.switch()
        time.sleep(0.1)


def c():  # 任务C
    for i in range(5):
        print('C' + str(i))
        ga.switch()
        time.sleep(0.1)


if __name__ == '__main__':
    ga = greenlet(a)
    gb = greenlet(b)
    gc = greenlet(c)

    ga.switch()
执行结果
A0
B0
C0
A1
B1
C1
A2
B2
C2
A3
B3
C3
A4
B4
C4

greenlet已经实现了协程,但是这个人工切换,是不是觉得太麻烦了,不要着急,python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent 其原理是当一个greentlet遇到IO(指的是input ouput输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO完成,再适当的时候切换回来继续执行。

由于IO操作非常耗时,经常使程序处于等待状态,有了gevent我们自动切换协程,就保证总有greenlet在运行,而不是等待IO

猴子补丁,将程序中用到的耗时操作的代码,换为gevent中自己实现的模块,如time.sleep(0.1),gevent中也有sleep()方法

一种在运行时候动态修改类,函数的功能的属性编程技巧

  • 1.在运行时候替换方法,属性

  • 2.在不修改第三方代码情况下,增加原来不支持对象

  • 3.运行时为内存中对象增加PATCH,不是在源代码磁盘中增加

示例代码

安装包 pip install gevent

python
import time
import gevent
from gevent import monkey
 
# 有耗时操作时需要
monkey.patch_all()  # 打补丁,将程序中用到的耗时操作的代码,换为gevent中自己实现的模块
 
 
def a():  # 任务A
    for i in range(5):
        print('A' + str(i))
        time.sleep(0.1)
 
 
def b():  # 任务B
    for i in range(5):
        print('B' + str(i))
        time.sleep(0.1)
 
 
def c():  # 任务C
    for i in range(5):
        print('C' + str(i))
        time.sleep(0.1)
 
 
if __name__ == '__main__':
    g1 = gevent.spawn(a)
    g2 = gevent.spawn(b)
    g3 = gevent.spawn(c)
 
    g1.join()
    g2.join()
    g3.join()
执行结果
A0
B0
C0
A1
B1
C1
A2
B2
C2
A3
B3
C3
A4
B4
C4

案例

安装包 pip install gevent

python
import requests
import gevent
from gevent import monkey

monkey.patch_all()


def download(url):
    response = requests.get(url)  # 耗时操作
    content = response.text
    print('下载了{}的数据,长度:{}'.format(url, len(content)))


if __name__ == '__main__':
    urls = ['http://www.163.com', 'http://www.baidu.com']
    g1 = gevent.spawn(download, urls[0])
    g2 = gevent.spawn(download, urls[1])

    g1.join()
    g2.join()

执行结果
 MonkeyPatchWarning: Monkey-patching ssl after ssl has already been imported may lead to errors, including RecursionError on Python 3.6. It may also silently lead to incorrect behaviour on Python 3.7. Please monkey-patch earlier. See https://github.com/gevent/gevent/issues/1016. Modules that had direct imports (NOT patched): ['urllib3.util (/Users/xxxxx/miniforge3/lib/python3.12/site-packages/urllib3/util/__init__.py)', 'urllib3.util.ssl_ (/Users/xxxxx/miniforge3/lib/python3.12/site-packages/urllib3/util/ssl_.py)']. 
  monkey.patch_all()
下载了http://www.baidu.com的数据,长度:2381
下载了http://www.163.com的数据,长度:311

上面有警告信息 这个警告是由 Python 的异步网络库 gevent 触发的。它的核心意思是:你对 ssl 模块进行猴子补丁(Monkey-patching)的时间太晚了。gevent 的工作原理是将 Python 标准库中原本阻塞的模块(如 socket、ssl、urllib 等)内部偷偷替换成非异步的版本(这个过程叫猴子补丁)。警告指出,在你的代码执行 monkey.patch_all() 之前,urllib3 已经把官方原始的、未经过修改的 ssl 模块导入到了内存中。这会导致新旧模块冲突,在 Python 3.7+ 版本中可能会引发隐蔽的逻辑错误,或者在 Python 3.6 中导致无限递归崩溃(RecursionError)。

修复这个问题非常简单:必须确保 gevent 的补丁代码放在整个程序的最顶部(第 1 行),比任何其他 import 语句都要早。

python
import gevent
from gevent import monkey
monkey.patch_all()

import requests


def download(url):
    response = requests.get(url)  # 耗时操作
    content = response.text
    print('下载了{}的数据,长度:{}'.format(url, len(content)))


if __name__ == '__main__':
    urls = ['http://www.163.com', 'http://www.baidu.com']
    g1 = gevent.spawn(download, urls[0])
    g2 = gevent.spawn(download, urls[1])

    g1.join()
    g2.join()

    # gevent.joinall(g1, g2)

执行结果
下载了http://www.baidu.com的数据,长度:2381
下载了http://www.163.com的数据,长度:311

总结选型:我该用多进程、多线程、还是 asyncio?

  • 计算、矩阵、图像处理、加密解密(CPU 密集) ➡️ 多进程 (multiprocessing)。
  • 本地大量读写文件、操作不支持异步的旧 C 语言库 ➡️ 多线程 (threading)。
  • 高并发 Web 服务器、网络爬虫、聊天室、API 网关(I/O 密集) ➡️ asyncio(单线程之王)。

并发中的死锁问题

在 Python 并发编程中,死锁(Deadlock) 是指两个或多个线程(Thread)、进程(Process)或协程(Coroutine)互相等待对方释放资源,导致程序永久卡死、无法继续执行的现象。

经典死锁场景1:多线程“交叉锁”(互锁)

这是最经典的死锁。线程 A 持有锁 1 尝试获取锁 2,而线程 B 持有锁 2 尝试获取锁 1。

❌ 错误示例(导致死锁)

python
import threading
import time

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

def thread_A():
    with lock1:
        print("线程 A: 已获取锁 1,正在等待锁 2...")
        time.sleep(0.1)  # 迫使线程 B 有时间获取锁 2
        with lock2:
            print("线程 A: 成功获取锁 2!")

def thread_B():
    with lock2:
        print("线程 B: 已获取锁 2,正在等待锁 1...")
        time.sleep(0.1)
        with lock1:
            print("线程 B: 成功获取锁 1!")

# 启动后程序会永久卡死
threading.Thread(target=thread_A).start()
threading.Thread(target=thread_B).start()

解决方案:固定锁的获取顺序 让所有线程以完全相同的顺序获取锁。如果线程 B 也先获取锁 1 再获取锁 2,死锁就不会发生。

python
import threading
import time

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

def thread_A():
    with lock1:
        print("线程 A: 已获取锁 1,正在等待锁 2...")
        time.sleep(0.1)  # 迫使线程 B 有时间获取锁 2
        with lock2:
            print("线程 A: 成功获取锁 2!")

# 修复线程 B 的顺序,使其与 A 一致
def thread_B_fixed():
    with lock1:  # 先拿锁 1
        print("线程 B: 已获取锁 1,正在等待锁 2...")
        with lock2:
            print("线程 B: 成功获取锁 2!")

# 启动后程序会永久卡死
threading.Thread(target=thread_A).start()
threading.Thread(target=thread_B_fixed).start()

经典死锁场景2:重入锁问题(单线程死锁)

当同一个线程尝试第二次获取它已经持有的普通锁(threading.Lock)时,它会被自己阻塞,导致死锁。

❌ 错误示例(导致死锁)

python
import threading

lock = threading.Lock()

def recursive_function(count):
    with lock:  # 第一层获取成功
        print(f"进入层级: {count}")
        if count > 0:
            # 递归再次尝试获取同一个 lock,但它已经被自己锁住了!
            recursive_function(count - 1) 

recursive_function(2)

解决方案:使用可重入锁 RLock Python 提供了 threading.RLock()(Reentrant Lock)。它允许同一个线程多次获取同一把锁,内部通过计数器维护。

python
import threading

# 修复:只需将 Lock 换成 RLock
lock = threading.RLock()

def recursive_function(count):
    with lock:  # 第一层获取成功
        print(f"进入层级: {count}")
        if count > 0:
            # 递归再次尝试获取同一个 lock,但它已经被自己锁住了!
            recursive_function(count - 1)

recursive_function(2)

经典死锁场景 3:asyncio 协程死锁

不要以为协程是单线程的就不会死锁!如果两个 asyncio.Lock() 互相等待,或者在协程中使用了阻塞的同步锁,同样会导致整个事件循环卡死。

❌ 错误示例(异步死锁)

python
import asyncio

async_lock1 = asyncio.Lock()
async_lock2 = asyncio.Lock()

async def task_A():
    async with async_lock1:
        print("任务 A 拿到了锁 1")
        await asyncio.sleep(0.1)
        async with async_lock2:  # 等待任务 B 释放锁 2
            print("任务 A 拿到了锁 2")

async def task_B():
    async with async_lock2:
        print("任务 B 拿到了锁 2")
        await asyncio.sleep(0.1)
        async with async_lock1:  # 等待任务 A 释放锁 1
            print("任务 B 拿到了锁 1")

async def main():
    await asyncio.gather(task_A(), task_B())

asyncio.run(main())

解决方案 :使用 asyncio.wait_for() 设置超时异步死锁最优雅的防御手段是加上超时机制,防止永久卡死。

python
try:
    # 如果 2 秒内拿不到锁,抛出 TimeoutError,打破死锁
    await asyncio.wait_for(async_lock2.acquire(), timeout=2.0)
    # 执行业务逻辑...
finally:
    if async_lock2.locked():
        async_lock2.release()

经典死锁场景 4:多进程 Queue 缓冲区满

在 Python multiprocessing 中,如果子进程向 Queue 写入大量数据(超过系统管道缓冲区限制),而主进程在调用 join() 之前没有读取数据,子进程就会被阻塞等待主进程读取,而主进程又在等待子进程结束,从而形成死锁。

❌ 错误示例(导致死锁)

python
from multiprocessing import Process, Queue

def worker(q):
    # 写入非常大的数据,塞满队列缓冲区
    q.put("X" * 1000000) 
    print("子进程写入完毕")

if __name__ == "__main__":
    q = Queue()
    p = Process(target=worker, args=(q,))
    p.start()
    
    # ❌ 错误:在读取数据前先 join()。
    # 此时子进程卡在 put() 等待缓冲区释放,而主进程卡在 join() 等待子进程结束。
    p.join() 
    
    data = q.get()
    print("主进程获取数据")

解决方案:先读数据,后 join

python
from multiprocessing import Process, Queue


def worker(q):
    # 写入非常大的数据,塞满队列缓冲区
    q.put("X" * 1000000)
    print("子进程写入完毕")


if __name__ == "__main__":
    q = Queue()
    p = Process(target=worker, args=(q,))
    p.start()
    data = q.get()
    p.join()

    print("主进程获取数据")

总结:如何预防和排查 Python 死锁?

  • 设置获取锁的超时时间(Timeout): 无论线程还是协程,尽量避免无限期等待锁。

    • 线程:lock.acquire(timeout=5.0)
    • 协程:asyncio.wait_for(lock.acquire(), timeout=5.0)
  • 尽量减少锁的粒度: 使用 with lock: 上下文管理器,确保锁在离开代码块、或者发生异常时能自动释放。切忌手动 acquire() 后因为隐式报错而漏掉 release()。

  • 排查利器:sys._current_frames(): 如果您的程序在生产环境中无故卡死,可以引入 faulthandler 模块,或者在卡死时打印所有线程的堆栈信息,一眼就能看出哪个线程卡在了哪把锁上:

    import faulthandler faulthandler.enable() # 程序崩溃或收到特定信号时会打印所有线程堆栈


进程(Process)、线程(Thread)、协程(Coroutine)通信问题

在 Python 中,进程(Process)、线程(Thread)、协程(Coroutine) 的并发级别不同,它们的数据共享和通信机制也完全不同。以下为您梳理三种并发模式下,最标准、最高效的通信(数据交换)方式与避坑指南。

进程间通信(IPC)

核心特点:进程之间内存完全隔离。不能通过全局变量直接通信,必须通过序列化(Pickle)进行数据传输。

(1)multiprocessing.Queue(多进程队列)

python
from multiprocessing import Process, Queue

def worker(q):
    # 子进程放入数据
    q.put({"result": "success", "data": [1, 2, 3]})

if __name__ == "__main__":
    q = Queue()
    p = Process(target=worker, args=(q,))
    p.start()
    
    # 主进程获取数据
    data = q.get()
    print(f"主进程收到: {data}")
    p.join()

执行结果
主进程收到: {'result': 'success', 'data': [1, 2, 3]}

(2)multiprocessing.Pipe(管道)

适用于两个进程之间的双向或单向快速通信(效率比 Queue 略高)。

python
from multiprocessing import Process, Pipe

def worker(conn):
    conn.send("来自子进程的问候")
    conn.close()

if __name__ == "__main__":
    parent_conn, child_conn = Pipe()
    p = Process(target=worker, args=(child_conn,))
    p.start()
    
    print(parent_conn.recv()) # 主进程接收
    p.join()

线程间通信

核心特点:同一进程内的线程共享全部全局内存。可以直接读写全局变量,但必须注意线程安全(GIL 无法保护应用层的数据竞争)。

(1)queue.Queue(线程标准队列)

python
import threading
import queue
import time

q = queue.Queue()


def producer():
    for i in range(3):
        print(f"生产了:产品 {i}")
        q.put(f"产品 {i}")
        time.sleep(0.5)


def consumer():
    while True:
        item = q.get()  # 如果队列为空,会自动阻塞等待
        if item is None:
            break
        print(f"消费了: {item}")
        q.task_done()


# 启动线程
t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)
t1.start()
t2.start()

t1.join()
q.put(None)  # 发送结束信号通知消费者退出
t2.join()

执行结果

生产了:产品 0
消费了: 产品 0
生产了:产品 1
消费了: 产品 1
生产了:产品 2
消费了: 产品 2

(2)共享变量 + threading.Lock(锁机制)

如果必须直接修改同一个全局变量(如计数器),必须加锁。

python
import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(100000):
        with lock: # 确保同一时间只有一个线程修改 counter
            counter += 1

协程间通信

核心特点:协程运行在同一个线程内,是单线程内的并发。不需要考虑多线程的物理死锁,但需要防止“逻辑阻塞”。

asyncio.Queue(异步队列)

与线程队列类似,但它的 put() 和 get() 都是异步协程,使用 await 进行非阻塞等待。

python
import asyncio

async def producer(q):
    for i in range(3):
        await q.put(f"消息 {i}")
        await asyncio.sleep(0.1)
    
    # 🌟 生产结束,放入一个 None 告知消费者可以下班了
    await q.put(None)

async def consumer(q):
    while True:
        item = await q.get()
        
        # 🌟 如果收到 None,说明后面没消息了,直接退出
        if item is None:
            q.task_done()
            break
            
        print(f"处理协程消息: {item}")
        q.task_done()

async def main():
    q = asyncio.Queue()
    await asyncio.gather(producer(q), consumer(q))

asyncio.run(main())


跨越界限:多线程与协程如何通信?

在实际开发中(如爬虫、GUI 应用),经常会遇到一个线程在跑 asyncio 事件循环,另一个普通线程想给它发数据的情况。

  • 绝对不能:直接在普通线程中调用 asyncio 的 await 或修改异步变量。
  • 正确做法:使用 asyncio.run_coroutine_threadsafe()。
python
import asyncio
import threading
import time

async def async_worker(q):
    """在异步事件循环中运行的消费者"""
    while True:
        item = await q.get()
        print(f"异步循环收到来自普通线程的数据: {item}")
        if item == "STOP":
            break

def start_loop(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()

if __name__ == "__main__":
    # 1. 在单独的子线程中启动 asyncio 事件循环
    new_loop = asyncio.new_event_loop()
    t = threading.Thread(target=start_loop, args=(new_loop,), daemon=True)
    t.start()

    # 2. 在该循环中初始化一个异步队列
    async_queue = asyncio.run_coroutine_threadsafe(asyncio.Queue(), new_loop).result()
    
    # 3. 在异步循环中启动消费者任务
    asyncio.run_coroutine_threadsafe(async_worker(async_queue), new_loop)

    # 4. 主线程(普通同步线程)向异步队列推送数据
    for i in range(3):
        time.sleep(0.5)
        # 核心:跨线程向异步队列中 put 数据
        asyncio.run_coroutine_threadsafe(async_queue.put(f"数据-{i}"), new_loop)

    # 发送停止信号
    asyncio.run_coroutine_threadsafe(async_queue.put("STOP"), new_loop)
    time.sleep(1)


IO与计算密集型

在 Python 开发中,I/O 密集型(I/O-bound)和计算密集型(CPU-bound)是两种截然不同的程序运行模式。由于 Python 存在 GIL(全局解释器锁),选择错误的并发方案不仅无法提升性能,甚至可能导致程序变慢。

I/O 密集型 (I/O-bound)

  • 定义:程序的主要瓶颈在于等待外部输入/输出(I/O),而 CPU 长期处于闲置(Sleep)状态。
  • 典型场景:
    • 网络爬虫、API 请求。
    • 数据库读写(SQL 查询)。
    • 磁盘文件读写(尤其是大文件读写)。
    • 网络套接字(Socket)通信。
  • 核心特征:CPU 在微秒内处理完代码,然后花几百毫秒等待网络或硬盘响应。

计算密集型 (CPU-bound)

  • 定义:程序的主要瓶颈在于密集的 CPU 数值计算或逻辑处理,CPU 使用率通常会飙升到 100%。
  • 典型场景:
    • 图像/视频处理、音视频转码。
    • 机器学习/深度学习模型训练。
    • 大数据清洗、高维矩阵运算。
    • 复杂的加密/解密、科学计算、大数阶乘。
  • 核心特征:程序运行期间,CPU 核心始终在全力运转,几乎没有等待时间。

Python 的核心死结:GIL 锁

Python(准确地说是 CPython 解释器)有一把 GIL(Global Interpreter Lock,全局解释器锁)。

  • 规则:在任意一个时刻,只允许一个线程执行 Python 字节码。
  • 对 I/O 密集型的影响:无所谓。 因为当一个线程在等待网络响应(I/O 阻塞)时,它会主动释放 GIL 锁,让其他线程进来用 CPU。因此,多线程对 I/O 密集型非常有效。
  • 对计算密集型的影响:灾难性。 如果用多线程跑计算,多个线程会为了争夺唯一的一把 GIL 锁而引发频繁的线程切换。不仅无法利用多核 CPU,反而会因为切换开销导致速度比单线程更慢。

解决方案与技术选型

针对这两种不同的密集型任务,Python 提供了三种并发技术:多进程(Multiprocessing)、多线程(Threading)、协程(Asyncio)。 场景与技术选型

任务类型 推荐技术方案 为什么?
I/O 密集型 协程 (asyncio) (或多线程 threading) 极致的高并发。协程在单线程内切换,没有线程上下文切换的开销。一个协程等待 I/O 时,另一个协程立刻顶上。
计算密集型 多进程 (multiprocessing) 绕过 GIL 锁。每个 Python 进程都有自己独立的解释器和独立的 GIL 锁,可以真正做到利用多核 CPU 核心进行并行计算。