并发编程入门:GIL锁的本质、多线程与多进程的选择、concurrent.futures实战
量化交易系统里,并发是个绕不开的话题。
你想想看,一边要拉行情数据,一边要做策略计算,另一边还得下单交易。如果所有事情都排队等着,那延迟就上去了。我刚开始做量化系统时,就吃过这个亏——单线程跑,行情来了,策略还在算上一笔,结果错过了最佳买卖点。
所以,咱们今天就来聊聊并发编程。我会从GIL锁讲起,再说说多线程和多进程怎么选,最后用concurrent.futures给你演示一套实战方案。
GIL锁:Python的“紧箍咒”
很多新手会问:Python不是支持多线程吗?为什么我的多线程程序跑起来反而更慢了?
答案就在GIL(Global Interpreter Lock,全局解释器锁)上。
说白了,GIL就是Python解释器里的一把大锁。它规定:同一时刻,只有一个线程能执行Python字节码。哪怕你有8个CPU核心,Python多线程也只能在一个核上跑。
为什么会这样?
这跟Python的内存管理有关。Python的对象引用计数是全局的,如果不加锁,多个线程同时修改引用计数,内存就乱套了。CPython(最主流的Python实现)选择用GIL来保证线程安全,简单粗暴。
核心结论:
- CPU密集型任务(比如策略计算、回测)——多线程没用,甚至更慢
- I/O密集型任务(比如网络请求、读写文件)——多线程有效,因为等待I/O时会释放GIL
我在项目中遇到过一件事:有个同事写了一个多线程的行情订阅程序,结果发现CPU占用率只有100%(单核满),其他核心都在闲着。他跑来问我怎么回事。我让他把线程数改成进程数,问题就解决了。
避坑指南:
我曾经在回测框架里用了多线程,以为能加速。结果跑起来比单线程还慢,因为线程切换有开销,GIL又限制了并行。后来改成多进程,速度直接翻了4倍(4核机器)。
多线程 vs 多进程:怎么选?
这个问题其实很简单,看你的任务类型。
| 维度 | 多线程 | 多进程 |
|---|---|---|
| 适用场景 | I/O密集型(网络、磁盘) | CPU密集型(计算、回测) |
| 内存共享 | 容易(共享全局变量) | 困难(需要IPC机制) |
| 启动开销 | 小 | 大(每个进程独立内存) |
| GIL影响 | 受限制 | 不受影响 |
| 调试难度 | 中等(竞态条件) | 较低(进程隔离) |
我个人习惯这样判断:
- 如果任务大部分时间在等网络、等磁盘——用多线程
- 如果任务大部分时间在算数学、算指标——用多进程
- 如果两者都有——用多进程+异步I/O的组合
举个例子。量化交易里,行情推送是I/O密集型的,用多线程就够了。但策略回测是CPU密集型的,必须上多进程。
concurrent.futures实战:写起来真舒服
Python标准库里的concurrent.futures模块,是我最推荐的并发工具。它封装了线程池和进程池,接口统一,用起来特别顺手。
先看一个多线程的例子:批量拉取股票行情。
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
def fetch_stock_price(stock_code):
# 模拟网络请求
time.sleep(1)
return f"{stock_code}: 100.5"
stock_codes = ["000001", "000002", "000003", "000004", "000005"]
# 创建线程池,最大并发5个
with ThreadPoolExecutor(max_workers=5) as executor:
# 提交任务
futures = {executor.submit(fetch_stock_price, code): code for code in stock_codes}
# 收集结果
for future in as_completed(futures):
code = futures[future]
try:
result = future.result()
print(f"完成: {result}")
except Exception as e:
print(f"失败: {code}, 错误: {e}")
这段代码里,ThreadPoolExecutor帮我们管理线程。你只需要提交任务,然后等结果就行。不用手动创建线程、不用管锁、不用管队列。
再看一个多进程的例子:并行回测多个策略。
from concurrent.futures import ProcessPoolExecutor
import time
def backtest_strategy(params):
# 模拟回测计算
time.sleep(2)
return f"策略{params}: 年化收益15%"
strategies = ["策略A", "策略B", "策略C", "策略D"]
# 创建进程池
with ProcessPoolExecutor(max_workers=4) as executor:
results = executor.map(backtest_strategy, strategies)
for result in results:
print(result)
注意,这里用的是ProcessPoolExecutor,不是ThreadPoolExecutor。接口几乎一样,但底层用的是进程。
重要提醒:
多进程里,子进程不能直接访问父进程的全局变量。如果你需要共享数据,要用multiprocessing.Manager或者Queue。我刚开始用多进程时,就踩过这个坑——子进程改了全局变量,父进程完全不知道。
实战中的选择策略
在量化交易系统里,我一般这样安排:
- 行情接收模块:多线程。因为网络I/O是瓶颈,线程数可以设大一点(比如10-20个)
- 策略计算模块:多进程。每个策略一个进程,充分利用多核CPU
- 订单管理模块:单线程+异步。订单有顺序要求,不能乱
这里有个小技巧:concurrent.futures的max_workers参数,不是越大越好。线程太多,上下文切换开销就上来了。进程太多,内存就爆了。我一般设成CPU核心数的2倍(I/O密集型)或者等于核心数(CPU密集型)。
知识体系总览
下面这张图,帮你理清本章的核心逻辑:
嗯,这张图把今天的内容串起来了。GIL是底层限制,多线程和多进程是两种武器,concurrent.futures是使用它们的最佳实践。
最后说一句:并发编程不是银弹。如果你的单线程程序已经够快了,别为了并发而并发。我见过太多人,为了炫技把简单问题搞复杂了。
本章要点:
- GIL让Python多线程无法并行执行CPU任务
- I/O密集型用多线程,CPU密集型用多进程
- concurrent.futures提供统一的线程池/进程池接口
- 实战中根据模块特性选择不同的并发策略