上一章 我们讲了架构设计模式:用于应对复杂项目中一些独特挑战的模式。接下来需要讨论并发与异步模式,这是我们的解决方案目录中另一类重要内容。
并发 让程序能够同时管理多项操作,充分利用现代处理器的能力。它好比一位厨师并行准备多道菜,每个步骤都被编排好,确保所有菜同时出锅。异步编程则让应用在等待某个操作完成时,先去处理别的事情------像把点单交给后厨后,服务员继续招待其他顾客,等菜好了再端上来。
在本章中,我们将讨论以下主题:
- 线程池(Thread Pool)模式
- Worker 模型模式
- Future 与 Promise 模式
- 响应式编程中的观察者(Observer)模式
- 其他并发与异步模式
技术要求
请参考第 1 章给出的通用要求。除此之外,本章代码还需要:
- Faker(使用
pip install faker
安装) - ReactiveX(使用
pip install reactivex
安装)
线程池(Thread Pool)模式
首先要理解什么是线程。在计算中,线程是操作系统可调度的最小处理单元。
线程就像可以同时在计算机上运行的多条"执行轨道",让许多活动并行进行,从而提升性能。它们在需要多任务处理的应用中尤为重要,比如同时服务多条 Web 请求或并行执行多项计算。
现在来看线程池模式 本身。设想有很多任务要完成,而启动每个任务(这里指为每个任务创建一个线程)在资源和时间上都很昂贵。这就像每来一件活就新招一个员工,活干完立刻解雇------既低效又花钱。通过维护一个可复用的工作线程集合(线程池) ,我们可以显著降低这种开销:当某个线程完成任务后,不会退出,而是回到池中等待下一项任务。
什么是工作线程?
**工作线程(worker thread)**是执行特定任务(或任务集)的线程。它们把耗时或资源密集的处理从主线程中卸出来,以异步方式执行,从而保持应用的响应性。
除了性能更快之外,还有两点好处:
- 开销更低:复用线程,避免为每个任务创建/销毁线程的开销。
- 更好的资源管理:线程池限制线程数量,避免一次创建过多线程导致资源耗尽。
现实类比
现实中,可类比为一家小餐馆有有限数量的厨师(线程)为顾客做菜(任务) 。厨房空间(系统资源)有限,同时工作的厨师数量也有限。新订单到来时,如果所有厨师都在忙,订单就在队列中等待,直到有空闲厨师接单。这样餐馆就能在有限厨师数内高效运转,而不必为每张订单都临时招聘。
软件中的常见例子
- Web 服务器使用线程池处理入站请求:并发服务多个客户端,同时避免为每个请求都新建线程的开销。
- 数据库用池化来管理连接:为入站查询保持可用连接集合。
- 任务调度器使用线程池执行定时任务,如 cron、备份或更新。
线程池模式的典型场景
- 批处理:有大量可并行的任务时,线程池可把它们分发给工作线程。
- 负载均衡:把工作均匀分配给各个线程,避免单线程过载。
- 资源优化:复用线程,降低内存与 CPU 的总体占用。
线程池的工作机制
- 初始化:应用启动时,线程池创建一定数量的工作线程。该数量可以固定,也可以按需动态调整。
- 提交任务:有任务时提交给线程池,而不是直接新建线程。任务可以是处理用户输入、网络请求或计算等。
- 执行任务:线程池把任务分派给空闲线程;若都在忙,任务在队列中等待。
- 复用线程:线程完成任务后不会退出,而是回到池中等待下一次分派。
示例代码
下面的示例使用 concurrent.futures
模块中的 ThreadPoolExecutor
创建一个包含 5 个工作线程的线程池来处理一组任务。
python
from concurrent.futures import ThreadPoolExecutor
import time
def task(n):
print(f"Executing task {n}")
time.sleep(1)
print(f"Task {n} completed")
with ThreadPoolExecutor(max_workers=5) as executor:
for i in range(10):
executor.submit(task, i)
运行示例(例如执行 python ch07/thread_pool.py
)时,你可能会看到如下输出(顺序可能不同):
arduino
Executing task 0
Executing task 1
Executing task 2
Executing task 3
Executing task 4
Task 0 completed
Task 4 completed
Task 3 completed
Task 1 completed
Executing task 6
Executing task 7
Executing task 8
Task 2 completed
Executing task 5
Executing task 9
Task 8 completed
Task 6 completed
Task 9 completed
Task 5 completed
Task 7 completed
可以看到,任务完成的顺序与提交顺序不同,这表明它们由线程池中的多个线程并发执行。
Worker 模型模式(The Worker Model pattern)
核心思想 :把一个大型任务或大量任务拆分为更小、可管理的工作单元(workers),并行处理。这样不仅能加速处理时间,也能显著提升应用性能。
Workers 可以是同一进程内的线程 (如上一节的线程池)、同一机器上的独立进程 ,甚至是分布式系统中不同机器上的执行单元。
优点
- 可扩展性(Scalability) :通过增加 worker 数量即可扩容,特别适合把任务分散到多台机器的分布式系统。
- 效率(Efficiency) :把任务分发到多个 worker 并行处理,更充分地利用计算资源。
- 灵活性(Flexibility) :既能用简单的线程式 worker,也能扩展到跨多台服务器的复杂分布式系统。
现实类比与软件实例
- 快递派送:包裹(任务)由快递员(workers)从分发中心(任务队列)领取并派送。旺季可增员,淡季可减员。
- 大数据处理:常见做法是由多个 worker 分别承担数据的 map 或 reduce 部分。
- 消息队列(RabbitMQ / Kafka) :从队列并发拉取消息进行处理。
- 图像处理服务:需要同时处理多张图片时,用多个 worker 分担负载。
典型适用场景
- 数据转换:海量数据需要统一转换时,把数据切分后分发给多个 worker 并行处理。
- 任务并行:应用中存在彼此独立的不同任务时,Worker 模型尤其高效。
- 分布式计算:把 Worker 模型扩展到多机环境,适配分布式场景。
实现思路
Worker 模型通常包含三类组件:workers、任务队列(task queue) ,以及可选的调度器(dispatcher) :
- Workers:主体执行者。每个 worker 可独立处理一部分任务;实现上可一次处理一个或并发处理多个。
- Task Queue :存放待处理任务的中心队列。workers 通常主动拉取任务,从而把"提交任务"与"处理任务"解耦,并实现高效分发。
- Dispatcher(可选) :按可用性、负载或优先级把任务指派给 workers,以进一步优化分配与资源利用。
示例:用多进程并行执行任务
思路 :用一个 multiprocessing.Queue
作为任务队列,启动多个进程(workers)从队列取任务并处理,直到队列为空。
css
from multiprocessing import Process, Queue
import time
def worker(task_queue):
while not task_queue.empty():
task = task_queue.get()
print(f"Worker {task} is processing")
time.sleep(1)
print(f"Worker {task} completed")
def main():
task_queue = Queue()
for i in range(10):
task_queue.put(i)
processes = [
Process(target=worker, args=(task_queue,))
for _ in range(5)
]
# 启动 workers
for p in processes:
p.start()
# 等待全部结束
for p in processes:
p.join()
print("All tasks completed.")
if __name__ == "__main__":
main()
可能的运行输出 (python ch07/worker_model.py
):
csharp
Worker 0 is processing
Worker 1 is processing
Worker 2 is processing
Worker 3 is processing
Worker 4 is processing
Worker 0 completed
Worker 5 is processing
Worker 1 completed
Worker 6 is processing
Worker 2 completed
Worker 7 is processing
Worker 3 completed
Worker 8 is processing
Worker 4 completed
Worker 9 is processing
Worker 5 completed
Worker 6 completed
Worker 7 completed
Worker 8 completed
Worker 9 completed
All tasks completed.
可以看到,5 个 workers 并发从任务队列领取并处理任务,直到 10 个任务全部完成。这正是 Worker 模型在"任务独立、可并行"场景下的高效用法。
Future 与 Promise 模式
在异步编程范式 中,Future 表示一个尚未知晓、但最终会得到 的值。函数启动异步操作时,不会阻塞等待其完成并返回结果,而是立刻返回一个 Future 。这个 Future 对象相当于对"稍后可用的实际结果"的占位符。
Future 常用于 I/O 操作、网络请求以及其他异步运行的耗时任务。它们让程序能够继续执行其他工作,而不是等待操作完成------这种特性称为非阻塞 。
一旦 Future 被"兑现"(完成),就可以通过回调、轮询,或在需要时阻塞等待的方式获取其结果。
Promise 是与 Future 配对的可写控制端 ,表示异步操作的生产者一侧 ,最终会把结果提供给关联的 Future。操作完成时,Promise 要么以某个值成功兑现 (fulfill),要么以错误拒绝 (reject),从而解析(resolve)对应的 Future。
Promise 支持链式 使用,可以把一系列异步操作清晰、简洁地串联起来。
允许程序在等待异步操作时继续前进,使应用更响应灵敏 。另一个好处是可组合性 :可以以干净、可管理的方式把多个异步操作组合、串联或并行执行。
现实类比
- 订制餐桌 :你向木匠下单后,得到预计完成日期与设计草图(Future),这代表木匠对交付的承诺 。木匠施工推进即 Promise 正在兑现;成品交付即解析该 Future,承诺被履行。
- 网购物流跟踪:下单后立即得到订单确认与物流单号(Future)。订单的处理/发货/签收状态(Promise 的兑现过程)实时更新,最终解析为"已送达"。
- 外卖应用:下单后得到预计送达时间(Future)。从备餐到取餐再到送达(Promise 持续兑现),最终餐到你手里,Future 被解析。
- 客服工单:提交工单后立即得到工单号与"将尽快回复"的消息(Future)。后台按优先级处理;回复到达即兑现最初的 Promise。
典型适用场景
- 数据管道:多阶段的数据转换中,用 Future 表示各阶段的结果。后续阶段以前一阶段的输出为输入,但不必阻塞等待------因为每一阶段都返回 Future,可并行推进。
- 任务调度 :系统或应用在调度"未来执行"的任务时返回 Future,代表该任务的最终完成状态,便于在不阻塞的前提下跟踪任务状态。
- 复杂数据库查询/事务:异步发起查询,立即把控制权交还给 UI 或调用方;Future 最终解析为查询结果,期间应用不会因等待数据库而"卡死"。
- 文件 I/O:把文件读写放到后台执行,返回一个 Future 表示其完成。应用可继续处理其他工作或用户交互;I/O 完成后 Future 解析,再处理或展示文件数据。
以上场景都体现出:通过不阻塞主线程处理长时任务,应用可以保持高响应与高效率。
用 concurrent.futures
实现 Future/Promise
理解实现前,先看其三步机制:
- 启动(Initiation) :启动异步操作的函数立即返回一个 Future 作为"稍后可用结果"的占位符。函数内部创建与之关联的 Promise ,由它来接收异步操作的成功结果或错误 ,并影响该 Future 的状态。
- 执行(Execution) :异步任务独立于主流程进行,程序可继续其它工作。任务完成后,结果(或错误)传给之前创建的 Promise。
- 解析(Resolution) :若成功,Promise 以结果兑现 ;若失败,以错误拒绝 。Promise 的兑现/拒绝解析(resolve)对应的 Future。随后可通过回调/续延(continuation)来"如何使用结果"。
示例中,我们用 ThreadPoolExecutor
异步执行任务;submit
会返回一个 Future,最终包含计算结果:
python
from concurrent.futures import ThreadPoolExecutor, as_completed
def square(x):
return x * x
with ThreadPoolExecutor() as executor:
future1 = executor.submit(square, 2)
future2 = executor.submit(square, 3)
future3 = executor.submit(square, 4)
futures = [future1, future2, future3]
for future in as_completed(futures):
print(f"Result: {future.result()}")
运行(例如 python ch07/future_and_promise/future.py
)可能得到:
makefile
Result: 16
Result: 4
Result: 9
这就完成了一个基本实现。
用 asyncio
实现 Future/Promise
什么是 asyncio?
asyncio
提供对异步 I/O、事件循环、协程等并发相关能力的支持,用来高效地处理 I/O 密集型操作。
协程与 async/await
协程(coroutine)是一类可在某些点暂停/恢复 执行的特殊函数(async def
定义)。协程之间可以并发 进行;在协程内调用其他协程时使用 await
。
示例:
scss
import asyncio
async def square(x):
# 模拟 I/O 密集操作
await asyncio.sleep(1)
return x * x
async def main():
fut1 = asyncio.ensure_future(square(2))
fut2 = asyncio.ensure_future(square(3))
fut3 = asyncio.ensure_future(square(4))
results = await asyncio.gather(fut1, fut2, fut3)
for result in results:
print(f"Result: {result}")
if __name__ == "__main__":
asyncio.run(main())
运行(例如 python ch07/future_and_promise/async.py
)可能输出:
makefile
Result: 4
Result: 9
Result: 16
结果顺序可能不同,不可预测 ------这在并发/异步代码中是常态。这个简单示例表明:在需要高效处理 I/O 型任务(如爬虫、API 调用)时,asyncio
是实现 Future/Promise 模式的合适选择。
响应式编程中的观察者模式
观察者模式 (第 5 章"行为型设计模式"已介绍)用于在某个对象状态发生变化时,通知一个或一组对象。这种传统的观察者允许我们对对象的变化事件做出反应,能优雅解决许多情形。但当我们需要处理大量事件 (其中有些还彼此依赖)时,传统做法可能会导致代码复杂且难以维护 。这正是另一种范式------响应式编程(reactive programming) ------提供有趣选项的地方。简单说,响应式编程的理念是:在保持代码整洁的同时,对**大量事件(事件流)**做出反应。
让我们聚焦在响应式编程的一部分 ReactiveX (reactivex.io)。ReactiveX的核心概念是 Observable(可观察序列) 。官方描述称,ReactiveX 提供了一套用于异步编程 的 API,其基础就是"可观察的流"。这个概念是对我们已讨论过的观察者思想的扩展。
可以把 Observable 想象成一条数据/事件之河 ,不断向 Observer(观察者) 下游输送元素。Observable 会一个接一个地发出条目,这些条目沿着由不同步骤/操作构成的"路径"流动,最终到达观察者,由其消费。
现实示例
- 机场航班信息显示系统 类似于响应式编程中的 Observable。系统持续流式更新"到达、起飞、延误、取消"等状态。观察者(旅客、航司员工、机场服务等)订阅该 Observable,并对连续的更新流作出反应,从而对实时信息进行动态应对。
- 电子表格应用 也是响应式编程的一种体现:在几乎所有电子表格中,交互式修改任意单元格,都会立即重新计算所有直接或间接依赖该单元格的公式,并更新显示。
ReactiveX 在多种语言中实现:Java(RxJava )、Python(RxPY )、JavaScript(RxJS )。Angular 框架就使用 RxJS 来实现 Observable 模式。
响应式观察者模式的适用场景
一个使用场景是 集合管道(collection pipeline) ,Martin Fowler 在博客(martinfowler.com/articles/co...)中曾讨论过。
Martin Fowler 对集合管道的描述
集合管道是一种编程模式:把计算组织为一连串可组合的操作,每一步以集合作为输入并产出集合,供下一步继续处理。
我们还可以用 Observable 对对象序列执行 map/reduce 或 groupby 等操作来处理数据。
此外,Observable 还能用于按钮事件、请求、Twitter 推文流等多种数据源。
在响应式编程中实现观察者模式
在这个示例中,我们基于一个文本文件 ch07/observer_rx/people.txt
(包含一组虚拟姓名)构建数据流,并在此之上创建一个 Observable。
注意
示例文件
ch07/observer_rx/people.txt
(包含虚拟人名)已提供。你也可以使用辅助脚本ch07/observer_rx/peoplelist.py
随时生成新的(下面会给出脚本)。
示例人名列表类似如下:
Peter Brown, Gabriel Hunt, Gary Martinez, Heather Fernandez, Juan White, Alan George, Travis Davidson, David Adams, Christopher Morris, Brittany Thomas, Brian Allen, Stefanie Lutz, Craig West, William Phillips, Kirsten Michael, Daniel Brennan, Derrick West, Amy Vazquez, Carol Howard, Taylor Abbott,
回到实现。先导入所需模块:
javascript
from pathlib import Path
import reactivex as rx
from reactivex import operators as ops
定义函数 firstnames_from_db()
,它从包含姓名的文本文件创建一个 Observable ,并对其进行一系列转换(之前见过的 flat_map()
、filter()
、map()
),以及一个新操作 group_by()
:发出文件中出现的名字 及其出现次数:
less
def firstnames_from_db(path: Path):
file = path.open()
# collect and push stored people firstnames
return rx.from_iterable(file).pipe(
ops.flat_map(
lambda content: rx.from_iterable(
content.split(", ")
)
),
ops.filter(lambda name: name != ""),
ops.map(lambda name: name.split()[0]),
ops.group_by(lambda firstname: firstname),
ops.flat_map(
lambda grp: grp.pipe(
ops.count(),
ops.map(lambda ct: (grp.key, ct)),
)
),
)
然后在 main()
中,定义一个每 5 秒发出一次数据 的 Observable,并把它与 firstnames_from_db(db_file)
的发射合并 (先设置 db_file
为人名文件):
css
def main():
db_path = Path(__file__).parent / Path("people.txt")
# Emit data every 5 seconds
rx.interval(5.0).pipe(
ops.flat_map(lambda i: firstnames_from_db(db_path))
).subscribe(lambda val: print(str(val)))
# Keep alive until user presses any key
input("Starting... Press any key and ENTER, to quit\n")
示例回顾(完整代码见 ch07/observer_rx/rx_peoplelist.py
):
- 导入所需模块与类。
- 定义
firstnames_from_db()
:从文本文件这一数据源构造 Observable,收集并推送人名的名(first name) 。 - 在
main()
中定义一个每 5 秒发射的 Observable,并与firstnames_from_db()
的输出合并。
运行:python ch07/observer_rx/rx_peoplelist.py
,示例输出(节选)可能如下:
vbnet
Starting... Press any key and ENTER, to quit
('Peter', 1)
('Gabriel', 1)
('Gary', 1)
('Heather', 1)
('Juan', 1)
('Alan', 1)
('Travis', 1)
('David', 1)
('Christopher', 1)
('Brittany', 1)
('Brian', 1)
('Stefanie', 1)
('Craig', 1)
('William', 1)
('Kirsten', 1)
('Daniel', 1)
('Derrick', 1)
当你按任意键并回车后,发射会被中断,程序退出。
处理新的数据流
上面的测试是静态 的:数据流仅限于当前文件中的内容。我们现在希望能生成多条数据流 。可以使用第三方模块 Faker (pypi.org/project/Fak...)来生成这类虚拟数据。下面是提供的示例脚本(ch07/observer_rx/peoplelist.py
):
python
from faker import Faker
import sys
fake = Faker()
args = sys.argv[1:]
if len(args) == 1:
output_filename = args[0]
persons = []
for _ in range(0, 20):
p = {"firstname": fake.first_name(), "lastname": fake.last_name()}
persons.append(p)
persons = iter(persons)
data = [f"{p['firstname']} {p['lastname']}" for p in persons]
data = ", ".join(data) + ", "
with open(output_filename, "a") as f:
f.write(data)
else:
print("You need to pass the output filepath!")
现在看看同时运行两个程序 (ch07/observer_rx/peoplelist.py
与 ch07/observer_rx/rx_peoplelis.py
)会发生什么:
- 在一个终端中运行数据生成脚本,并传入文件路径:
python ch07/observer_rx/peoplelist.py ch07/observer_rx/people.txt
- 在第二个终端中运行实现 Observable 的程序:
python ch07/observer_rx/rx_peoplelist.py
结果是什么?
people.txt
会被写入(用逗号分隔的随机姓名)。每次你再次运行数据生成脚本,都会往文件追加一组新的人名。- 第二个程序的输出与首次运行类似,但不再是同一批数据反复发射;现在,数据源会不断生成新数据并被发射出来。
其他并发与异步模式(Other concurrency and asynchronous patterns)
开发者还会用到一些其它并发与异步模式,例如:
- Actor 模型(Actor model) :一种处理并发计算的概念模型。它为 actor 实例的行为制定规则:一个 actor 可以做本地决策、创建更多 actor、发送消息,并决定如何响应下一条收到的消息。
- 协程(Coroutines) :一种通用控制结构,控制流在两个例程间协作式 地转移而无需常规的函数返回。协程通过可挂起/可恢复 执行来促进异步编程。正如前文示例所示,Python 通过
asyncio
内置对协程的支持。 - 消息传递(Message passing) :用于并行计算、面向对象编程(OOP)与进程间通信(IPC),软件实体通过相互传递消息来通信并协调行为。
- 背压(Backpressure) :一种管理系统中数据流、避免压垮组件的机制。它通过向生产者发出"减速"信号,使系统能优雅地处理过载,直至消费者赶上进度。
这些模式各有适用场景与权衡。了解它们的存在很有意义,但我们无法在一本书中把所有模式与技术都详尽展开。
小结(Summary)
本章讨论了并发与异步模式------它们有助于编写高效、响应灵敏、能同时处理多任务的软件。
- 线程池(Thread Pool)模式:并发编程的有力工具,通过复用受控数量的线程来执行任务,不仅提升性能,还降低创建/销毁线程的开销,并更好地管理资源。
- Worker 模型(Worker Model)模式 :与线程池强调"复用固定数量线程"不同,Worker 模型更侧重将任务动态分发 到可伸缩、灵活的工作单元上。它特别适合任务彼此独立、可并行处理的场景。
- Future 与 Promise 模式 :促进异步操作,使应用无需在长耗时任务上阻塞主线程,从而保持响应性与效率。
- 响应式编程中的观察者模式 :核心思想是对数据与事件流 做出反应,类似自然界中的水流。我们在计算领域有大量对应实例;本章以 ReactiveX 为例,为读者介绍这一编程范式,并可进一步查阅其官方文档以深入学习。
- 此外,我们也简要触及其他并发与异步模式。它们各有使用边界与取舍,难以在一本书内一一覆盖。
下一章 我们将讨论性能设计模式(performance design patterns) 。