2、行情处理流水线并行化设计:基础概念回顾
各位同学,在正式进入流水线并行化设计之前,咱们得先把地基打牢。我见过不少团队,一上来就撸代码搞并行,结果跑起来比单线程还慢——说白了,就是基础概念没吃透。今天这一章,咱们把进程、线程、协程、GIL、I/O模型这些老生常谈但又容易踩坑的东西,掰开揉碎了讲清楚。
2.1 进程、线程与协程:三兄弟的恩怨情仇
先问个问题:你写一个行情接收程序,每秒要处理10万笔订单,你会选哪个?
进程,是操作系统分配资源的最小单位。每个进程有自己的内存空间,互不干扰。好处是稳定——一个进程挂了,别的还能活。坏处呢?切换开销大,通信麻烦。我在项目中遇到过,用多进程做行情快照,结果进程间用管道传数据,延迟直接飙到毫秒级,后来改用共享内存才解决。
线程,是CPU调度的最小单位。同一进程内的线程共享内存,切换成本比进程低很多。但共享内存也带来了麻烦——你得加锁。我记得有一次,一个同事没处理好锁的粒度,行情数据被两个线程同时写,结果价格字段出现了「半新半旧」的脏数据,回测直接崩了。
协程,是用户态调度的轻量级「线程」。它不依赖操作系统,由程序自己控制切换。协程的切换成本极低,可以轻松创建上万个。但协程有个致命弱点:它不能利用多核CPU。说白了,协程是「单线程里的并发」,不是并行。
| 维度 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 资源开销 | 高(独立内存空间) | 中(共享内存) | 极低(栈级别) |
| 切换成本 | 高(内核态切换) | 中(内核态切换) | 极低(用户态切换) |
| 数据共享 | 难(需IPC) | 易(共享内存) | 易(同一线程内) |
| 适用场景 | CPU密集型、隔离性要求高 | I/O密集型、需要共享状态 | 高并发I/O、异步编程 |
2.2 GIL:Python并行路上的绊脚石
说到Python的并行,绕不开GIL(全局解释器锁)。GIL是什么?简单说,它保证同一时刻只有一个线程在执行Python字节码。你想想看,这意味着什么?多线程在Python里,其实是在「并发」而不是「并行」。
为什么会这样?Python的内存管理不是线程安全的。如果没有GIL,两个线程同时操作引用计数,内存就乱套了。所以CPython选择了GIL这个「简单粗暴」的方案。
我曾经在一个量化团队里,看到有人用Python多线程做行情解码,开了8个线程,结果CPU利用率只有120%(单核100%+其他核零星跑)。他跑来问我为什么性能没提升,我说:「你把GIL忘了。」后来改成多进程,CPU利用率直接飙到700%。
2.3 同步与异步I/O模型:别让CPU等你
同步I/O,就是程序发起一个读操作,然后傻等,直到数据回来才继续往下走。异步I/O呢?发起读操作后,程序继续干别的事,数据到了再通知你。
你想想看,行情处理中,网络延迟是最大的瓶颈。如果每个行情包都用同步I/O去读,CPU大部分时间都在空转。异步I/O可以让CPU在等待网络数据的同时,去处理已经到手的行情。
Python里实现异步I/O,主流方案是asyncio。它基于事件循环和协程,可以轻松管理成千上万个网络连接。
import asyncio
async def handle_market_data(reader, writer):
while True:
data = await reader.read(1024) # 异步读取,不阻塞
if not data:
break
# 处理行情数据
process(data)
async def main():
server = await asyncio.start_server(
handle_market_data, '0.0.0.0', 8888)
async with server:
await server.serve_forever()
asyncio.run(main())
2.4 并发与并行:一字之差,天壤之别
这两个词经常被混用,但本质完全不同。
并发,是逻辑上的同时执行。一个CPU核,通过快速切换任务,让你感觉多个任务在同时跑。就像一个人同时看三本书——其实是在快速切换阅读。
并行,是物理上的同时执行。多个CPU核,每个核跑一个任务,真正的同时进行。就像三个人各看一本书。
行情处理中,并发和并行都有用武之地。并发用于处理多个网络连接(比如同时接收沪深两市的行情),并行用于加速计算(比如同时计算多个策略信号)。
2.5 本章知识体系总览
下面这张图,把本章的核心概念串起来了。你可以看到,进程、线程、协程在资源开销和切换成本上的差异,以及GIL如何影响Python的并行能力。同步与异步I/O模型,决定了CPU的利用率。而并发与并行的本质差异,指导我们如何设计行情处理流水线。
公众号:蓝海资料掘金营,微信deep3321