4、信号捕捉引擎:事件驱动架构、信号队列设计、多线程处理
信号捕捉引擎,说白了就是整个盘口系统的「耳朵」和「大脑」。
耳朵要灵敏,不能漏掉任何一次报价变动。大脑要快,收到信号后得立刻判断:这玩意儿值不值得出手?
我个人习惯把信号引擎拆成三个核心模块:事件驱动架构、信号队列、多线程处理。咱们一个一个来拆解。
4.1 事件驱动架构:别轮询,等通知
很多新手做盘口监控,喜欢写个死循环,每秒去查一次行情。这其实很傻。
为什么?因为行情变化是毫秒级的。你轮询间隔设100ms,可能就错过了中间3次关键报价。设1ms轮询,CPU直接跑满,别的啥也干不了。
我早期做外汇盘口时,就吃过这个亏。当时用轮询方式监控欧元兑美元,结果有一次闪电波动,我的程序愣是没抓到入场点。后来复盘发现,轮询间隔刚好卡在报价变化间隙上。
事件驱动的核心思想: 行情来了才处理,行情不来就歇着。CPU利用率极低,响应速度极快。
具体实现上,我推荐用 观察者模式。行情源作为被观察者,信号引擎作为观察者。行情一变,引擎立刻被唤醒。
// 伪代码:事件驱动架构
class MarketDataPublisher {
List<SignalEngine> observers;
void onTick(TickData tick) {
for (engine : observers) {
engine.onMarketEvent(tick);
}
}
}
class SignalEngine implements MarketObserver {
void onMarketEvent(TickData tick) {
// 收到事件,立刻处理
processTick(tick);
}
}
嗯,这里要注意:事件驱动不是银弹。如果行情爆发时每秒来几千次事件,你的处理器会被淹没。这时候就需要信号队列来缓冲了。
4.2 信号队列设计:给事件装个缓冲池
信号队列,说白了就是个「蓄水池」。行情来得太快,处理器来不及消化,就先存到队列里。
我见过最糟糕的设计:直接用无界队列。行情一爆发,内存直接爆掉。有一次我在回测系统里这么干,跑了3小时,内存占用飙到8GB,程序直接OOM崩溃。
避坑指南: 信号队列一定要用有界队列!我曾经因为用无界队列,导致生产环境内存溢出,损失了一整天的交易数据。
队列设计有几个关键参数:
| 参数 | 说明 | 我的建议值 |
|---|---|---|
| 队列容量 | 最多能存多少条信号 | 10000 ~ 50000 |
| 丢弃策略 | 队列满了怎么办 | 丢弃最旧的信号 |
| 优先级 | 重要信号优先处理 | 大单异动 > 普通报价 |
我个人习惯用 环形缓冲区 来实现信号队列。为什么?因为内存是预分配的,没有动态分配的开销,性能极高。
// 环形缓冲区实现信号队列
class SignalQueue {
Signal[] buffer;
int head, tail, size;
boolean push(Signal signal) {
if (size == MAX_SIZE) {
// 队列满了,丢弃最旧的
head = (head + 1) % MAX_SIZE;
size--;
}
buffer[tail] = signal;
tail = (tail + 1) % MAX_SIZE;
size++;
return true;
}
Signal pop() {
if (size == 0) return null;
Signal s = buffer[head];
head = (head + 1) % MAX_SIZE;
size--;
return s;
}
}
小技巧: 环形缓冲区配合CAS操作,可以实现无锁队列。在高并发场景下,性能比加锁队列高3-5倍。
4.3 多线程处理:分工协作,各司其职
信号引擎如果单线程跑,遇到行情爆发就是灾难。你想想看:一个线程既要收行情、又要算指标、还要发信号,怎么可能快?
我建议至少拆成三个线程:
- 采集线程:专门收行情,往队列里塞
- 处理线程:从队列里取信号,计算指标
- 输出线程:把生成的交易信号发出去
这样做的好处是:采集线程不会被计算逻辑阻塞。哪怕处理线程卡住了,采集线程还能继续收行情。
我记得有一次做股指期货的盘口系统,行情爆发时每秒来8000笔报价。单线程处理延迟飙到200ms,根本没法用。改成三线程后,采集线程延迟稳定在1ms以内,处理线程虽然偶尔堆积,但整体吞吐量提升了10倍。
多线程核心原则: 线程之间通过队列通信,不要共享变量。共享变量意味着加锁,加锁意味着性能下降。
线程间通信的典型模式:
// 多线程架构
class SignalEngine {
BlockingQueue<TickData> inputQueue; // 采集→处理
BlockingQueue<Signal> outputQueue; // 处理→输出
// 采集线程
void collectorLoop() {
while (running) {
TickData tick = marketData.nextTick();
inputQueue.put(tick); // 阻塞式放入
}
}
// 处理线程
void processorLoop() {
while (running) {
TickData tick = inputQueue.take(); // 阻塞式取出
Signal signal = analyze(tick);
if (signal != null) {
outputQueue.put(signal);
}
}
}
// 输出线程
void outputLoop() {
while (running) {
Signal signal = outputQueue.take();
sendToTradingSystem(signal);
}
}
}
嗯,这里有个坑要注意:线程数量不是越多越好。我见过有人搞10个处理线程,结果线程切换开销比实际计算还大。一般来说,处理线程数 = CPU核心数 - 1 就够用了。
4.4 整体架构图
说了这么多,咱们用一张图把整个信号捕捉引擎串起来:
这张图把整个流程串起来了:行情从交易所进来,采集线程收,塞进输入队列。处理线程从队列里取,算完信号塞进输出队列。输出线程再发到交易系统。每个环节各司其职,互不干扰。
我的经验: 实际部署时,我会给每个线程绑定独立的CPU核心。比如4核机器,采集线程绑核心0,处理线程绑核心1,输出线程绑核心2,留一个核心给操作系统。这样能最大限度减少上下文切换。
好了,信号捕捉引擎的核心就这些。事件驱动让你不浪费CPU,信号队列让你扛得住爆发,多线程让你吞吐量翻倍。这三板斧用好了,盘口信号捕捉基本不会出大问题。
公众号:蓝海资料掘金营,微信deep3321