进程
进程是一个执行中的程序。每个进程都拥有自己的地址空间、内存、数据栈以及其他用于跟踪执行的辅助数据。在单核CPU系统中的多进程,内存中可以有许多程序,但在给定一个时刻只有一个程序在运行;就是说,可能这一秒在运行进程A,下一秒在运行进程B,虽然两者都在内存中,都没有真正同时运行。
线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。Python可以运行多线程,但和单核CPU多进程一样,在给定时刻只有一个线程会执行。
(1)利用Process来创建子进程
# -*- 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)利用进程池
# -*- 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 (进程池)供我们使用
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创建子线程
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)”保护打印(推荐)
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")
执行结果
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 拼接成一个完整的单字符串。在很多操作系统中,打印单个字符串是一个原子操作,被切断的概率会大大降低:
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")
执行结果
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
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
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 (线程池)供我们使用
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可以把一个函数的结果传给另外一个函数,以此实现单线程内程序之间的切换。
# -*- 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 关键字。
示例
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 版本中。
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()同时并发执行:
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的网站首页:
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一般形式创建
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.自动创建事件循环并运行协
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。
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):
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
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
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
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 语句都要早。
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。
❌ 错误示例(导致死锁)
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,死锁就不会发生。
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)时,它会被自己阻塞,导致死锁。
❌ 错误示例(导致死锁)
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)。它允许同一个线程多次获取同一把锁,内部通过计数器维护。
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() 互相等待,或者在协程中使用了阻塞的同步锁,同样会导致整个事件循环卡死。
❌ 错误示例(异步死锁)
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() 设置超时异步死锁最优雅的防御手段是加上超时机制,防止永久卡死。
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() 之前没有读取数据,子进程就会被阻塞等待主进程读取,而主进程又在等待子进程结束,从而形成死锁。
❌ 错误示例(导致死锁)
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
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(多进程队列)
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 略高)。
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(线程标准队列)
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(锁机制)
如果必须直接修改同一个全局变量(如计数器),必须加锁。
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 进行非阻塞等待。
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()。
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 核心进行并行计算。 |
评论
0 条