异步通知是什么意思_从零架构一个交易框架(五)—— 异步框架的一点杂谈...

主要还是从 rpc 框架说起,之前一直选用的是 zerorpc 作为 rpc 框架,然后在写 market 行情的部分的时候其实遇到了这个框架的第一个小问题就是它采用的是 gevent 作为异步 io 框架,而项目里用的是 asyncio 作为异步 io 框架,这就导致了需要做一些工作在两个框架之前倒腾。在完成 market 的时候还好,这里先记录一下两种不同异步框架的 ugly 的适配。然后再说说近期遇到的另一个坑。

先简单说说异步框架的原理

所有的异步框架最底层都是基于操作系统底层提供的 poll/epoll/select/kqueue 支持,这里不打算深入讲解代码和逻辑,只说说我们要怎么认识、怎么在心里定位异步框架就好,因为我们只要有正确的模型认识的话,就比较容易去排查异步框架的问题。更不用说这里涉及两个不同的异步框架之间的调用。

在程序跑的所有过程中,其实只有阻塞操作和非阻塞操作,非阻塞操作就像是一些纯计算,速度特别快,CPU/GPU 自己玩的飞起。通常大家在用电脑打大型3D游戏或者跑深度学习的时候,就会有大量的计算工作,非阻塞操作在于这些逻辑对 CPU/GPU 的使用率特别高。而阻塞操作就是相对的,一般都是一些 io 操作,比方说网络、硬盘等,在执行阻塞操作的时候基本上 CPU/GPU 都闲着磨洋工。所以为了提高运行效率,大家就折腾出很多不同的方案,比方说多进程多线程。

多进程、多线程

多进程和多线程就好比下棋的时候打车轮战一样,牛人 N 先和 A 对局,下了一步之后你 A 是需要慢慢的思考对吧,好那 N 就切换到和 B 的对弈上来,然后 B 在看到走棋之后也需要思考,那 N 再去找 C 下棋,依次类推直到 A 应对了一手,然后 N 再回来看看和 A 的盘面,走下一步。 CPU/GPU 也是这样,在发起一个网络 io 之后,不想等 io 慢吞吞地搬数据,就让这个进程/线程先挂起,然后处理别的进程、线程……然后做一些计算工作之后回来再看一眼网络 io 是不是做好了,没完成就继续切走看看别的操作是不是完成了。

协程

CPU/GPU 之前一直用多进程、多线程的方式做的挺好的,然后在计算机发展的近期又出现了协程的概念,这是什么意思呢?在刚才多进程、多线程的描述中我们会发现 CPU/GPU 利用率上来了,但还是做了很多无用工,就是它得一圈圈地轮询之前交代的 io 工作是否完成。这样会有很多上下文切换的开销,简单来说就是看上去 CPU/GPU 很忙了,但都是一些无用工。所以操作系统层就对 io 做了改进说:你 CPU/GPU 不用来一个一个问了,等 io 完成了我去通知你。这也就像在公司里一样,老板在下班的时候一个个问工作进度怎么样了总是很烦人,不如我们告诉老板等我们工作完成了主动去通知他,大家不都更开心了么。

刚才说的操作系统主动通知就是协程的基础,也就是上面提到的 poll/epoll/select/kqueue 做的事情。所以在协程中,用户代码里不再需要开多线程多进程,只需要单一进程单一线程就可以完成效率很高的工作了

题外话:对于 io 密集型(阻塞操作特别多的)逻辑可以很高效地用协程完成,也就是说在这种程序下,一个高频的单核处理器要比低频多核处理更有效率。但如果是对于 计算密集型的逻辑,比方说深度学习、mapreduce、大数据分析、大型3D渲染 等就是多核处理器更有效率,这也是为什么游戏、深度学习都喜欢用GPU的原因,因为GPU都是几千个核心同时在跑“有价值”的工作(而不是线程上下文切换这种没太多价值的工作)像是 3080ti 这种都有接近 1万个 CUDA 核心(还是一万多来着?)

回正题,无论是 gevent 还是 asyncio 就都是这样的逻辑,把所有的 io 交给操作系统之后,就可以去葛优瘫了,直到有 io 完成主动通知。

import 

为什么用 gevent.sleep 而不是 time.sleep 呢?就是因为 gevent.sleep 在底层用了 poll 那一系列的骚操作,所以可以实现把 io 的工作交付到操作系统层,而 gevent.spawn 就相当说告诉操作系统:嘿,这里有一个可能会涉及重 io 操作的工作,你里面的东西帮我记得处理一下。

最后在 "葛优瘫" 部分,其实真正的叫法应该是事件循环,就等着 操作系统唤醒了,所以这里也需要 gevent.sleep 来完成,对于操作系统来说,看到的所有异步 io 代码处,都可以想象成一个 "任意门",遇到了所有的 io 操作时,都可能会从这个任意门里跳到之前任意一个任意门处,只要那里对应的 io 完成了,如果没有完成就一直等待所有的任意门,直到某一个完成。

asyncio 也是同样的理解

import 

一样的代码不过多解释,对照标黑的文字看就好。

如何实现异步框架的互相调用呢?

虽说在底层都是操作系统在处理,但不同的框架之间的任意门是不互通的,我在 gevent 里创建的任意门就只能回到 gevent 里的任意门,在 asyncio 里创建的任意门就只能回到 asyncio 里。所以我们要做的其实就是打通 gevent 和 asyncio 的任意门而已。那么最外层的事件循环其实都是一样的,我们只需要选一个自己代码使用的主 异步框架 就好,比方说我们用的是 asyncio,然后调用 gevent 的代码(就好比我写的 livetrader 框架调用 zerorpc)就是这样

import 

中间的 global_loop 就是打通任意门的关键逻辑,在主事件循环里依次唤醒两个框架的 io 逻辑。但这里看上去还是两个 io 框架是分开的,所以如果我们要涉及到 asyncio 调用 gevent 的话,其实也很简单,就是这样:

import 

好了,铺垫了这么多,其实我想说的是我准备先把 zerorpc 框架用 asyncio 的方式实现一下

为什么呢?现在项目的进度是 market 代码已经完成mt4、通达信的对接,然后 trade 的代码完成了 mt4 的对接,但光说完成了没有一些实际的回测框架对接总觉得不靠谱,所以现在还完成了 backtrader 对 market、trade 的对接。然后现在的问题就是 backtrader 的主循环其实并不控制在我们的代码里面,而是控制在 backtrader 的回测/交易引擎里,所以有一个问题就是我们无法在主代码里完成事件循环,也因此我们需要在 store 的代码里开一个线程来做事件循环。

可以先看一下这个分支:livetrader-backtrader-connector

当代码写成这个样子的时候我们已经陷入了很深的 多线程/gevent/asyncio 的逻辑里了,大量的代码是在做这三种环境的适配。所以可能现在最重要的问题就是需要把 zerorpc 的逻辑用 asyncio 实现之后,简化 market/trade 的逻辑,然后只需要在 connector 里多线程调用 asyncio 就可以。这些是技术债务,不完成的话就会一直拖进度让我们的开发效率变的极低,正所谓磨刀不误砍柴工,现在要磨磨刀了


刘敬思:从零架构一个交易框架(四)——行情代码​zhuanlan.zhihu.com
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值