并发编程入门: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.futuresmax_workers参数,不是越大越好。线程太多,上下文切换开销就上来了。进程太多,内存就爆了。我一般设成CPU核心数的2倍(I/O密集型)或者等于核心数(CPU密集型)。

知识体系总览

下面这张图,帮你理清本章的核心逻辑:

并发编程知识体系 并发编程 GIL锁的本质 同一时刻单线程 I/O时释放GIL 多线程 vs 多进程 I/O密集型 CPU密集型 concurrent.futures ThreadPoolExecutor ProcessPoolExecutor 核心:根据任务类型选择并发模型

嗯,这张图把今天的内容串起来了。GIL是底层限制,多线程和多进程是两种武器,concurrent.futures是使用它们的最佳实践。

最后说一句:并发编程不是银弹。如果你的单线程程序已经够快了,别为了并发而并发。我见过太多人,为了炫技把简单问题搞复杂了。

本章要点:

  • GIL让Python多线程无法并行执行CPU任务
  • I/O密集型用多线程,CPU密集型用多进程
  • concurrent.futures提供统一的线程池/进程池接口
  • 实战中根据模块特性选择不同的并发策略

专注资料整理