Task 单线程多任务框架
多任务问题
对于用户而言, 一个任务中通常包含了若干操作和等待, 以一个简单的止损需求为例:
- 发出一个买入开仓报单指令
- 如果报单完全成交, 则: - 等待价格跌破 开仓价 - 30 元, 一旦条件满足, 立即发出一个平仓指令
- 如果用户撤销了报单, 则任务结束
在这个例子中, 等待发出的开仓指令成交, 和等待价格满足预设条件, 都需要等待一段较长的时间, 在这段时间内, 我们可能还有别的策略需要运行, 因此需要某种多任务机制, 来使多个任务同时执行. 常见的多任务方案有三种
异步回调+状态机模型
CTP API 使用的即是此方式. 每当一个事件发生时, 触发特定的回调函数. 用户在回调函数中编写自己的业务代码对事件进行响应.
Pros:
- 语法简单, 绝大多数编程语言都直接支持此类模型, 性能较高
Cons:
- 用户的业务代码被分成两个(或更多)部分, 一部分代码在主线程中执行, 另一部分代码放在回调函数中, 代码结构与需求结构不一致, 导致编码困难
- 当业务逻辑较复杂时, 需要用户自行构建状态机和管理状态变量
- 主线程和回调函数线程中的代码如果常常需要访问共同变量, 因此需用户实现转线程或线程锁机制
多线程阻塞模型
每个任务建立一个线程, 在线程中可以方便的执行阻塞和等待.
Pros:
- 代码结构与需求结构较为接近, 编码较简单
- 所有业务代码可以组织到一个函数中, 避免状态机和全局变量
Cons:
- 多线程都对共同数据集执行读写操作, 需要小心的使用锁机制
- 线程开销较大, 创建大量线程后性能明显下降
基于generator机制的单线程多任务模型
为了克服上面两种机制的困难, 现代编程语言中通常都支持某种形式的 单线程多任务 机制, 例如 golang 中的 coroutines, javascript 和 python 中的 generator 等.
Pros:
- 代码结构与线程函数相似, 所有业务代码可以组织到一个函数中, 避免状态机和全局变量
- 代码中明确插入等待事件的代码, 任务管理器只会在这个位置执行任务切换
- 多任务访问共同变量无需加锁
- 没有线程开销
Cons:
- 较老的编程语言对此机制缺乏支持
我们推荐使用这种模型, 并在 TQSDK 中对这种方式给予了专门支持
任务概念
我们将一个任务称为一个 Task. 在实现上, 每个 Task 是一个 Javascript Generator Function.
function* TaskQuote() { // 与普通函数不同, Task的关键字 ``function`` 和函数名中间必须有一个 ``*``
while (true) {
var result = yield { // 关键字 ``yield`` 表示,函数在异步执行的等待条件
UPDATED_QUOTE: function () { return !!TQ.GET_QUOTE(TQ.UI.instrument) },
CHANGED: TQ.ON_CHANGE('symbol')
};
var quote = TQ.GET_QUOTE(TQ.UI.instrument);
TQ.UI(quote); // 更新界面
}
}
任务管理器与任务调度
TQSDK 中实现了一个任务管理器, 来负责管理Task的生存周期和CPU切换.
Task的启动和停止
系统提供了 4 个函数操作 Task:
可以在任意位置开始、结束、暂停、恢复一个 Task,但是已经结束的 Task 无法恢复运行。可以选择重新开始一个 Task。
在 Task 中实现异步等待
在 Task 中使用 yield 实现异步等待. yield 后跟一个 object, 列出需要等待的条件。
object 的每个 Key 值对应一个条件,Key 值有两种情况:
- TIMEOUT: 后面直接跟等待超时的毫秒数。
- 其余 Key 值,根据用户习惯定义,值必须是一个返回
true
或者false
函数, TQSDK 在每次收到服务器发来的数据包时,都会检查 yield 后面的条件,其中至少有一个条件返回为true
时,程序才会继续运行, 直到遇到下一个 yield 为止。
通过这样的机制,就可以在 yield 后面添加任意条件,等待下单机会。
function* SomeTask() {
// do something...
let quote = TQ.GET_QUOTE("SHFE.cu1801");
var wait_result = yield { //关键字 ``yield`` 表示,函数在执行到这里时,会检查后面对象表示出的条件,并以对象形式返回,后面代码中就可以根据返回的内容执行不同的逻辑。
PRICE_HIGH: function () { return quote.last_price > 50000 }, // 当行情价格>50000时满足条件
STOPPED: TQ.ON_CLICKED('stop'), //当用户点击 stop 按钮时满足条件
TIMEOUT: 5000, //等待时间超过 5000 毫秒时满足条件
};
// 只有以上三个条件至少有一个返回值是 true 时, yield 才会返回一个 object, 记录了各条件的计算结果
/*
wait_result = {
PRICE_HIGH: false,
STOPPED: true,
TIMEOUT: false,
}
*/
}
Task的嵌套调用
调用 TQ.START_TASK(TaskChild) 可以返回一个 Task 对象。
Task 对象可以提供的属性:
task_child.stopped
可以获取 Task 对象是否运行结束。
task_child.return
可以获取 Task 对象运行结束后返回的值。
function* TaskParent() {
// do something
// ...
// start two child task
let task_child_1 = TQ.START_TASK(TaskChild);
let task_child_2 = TQ.START_TASK(TaskChild);
// wait until child tasks finish or user clicked stop
let wait_result = yield {
SUBTASK_ERROR: function (){ return task_child_1.return == 'error' && task_child_2.return == 'error'; }, //Any sub task occur errors
SUBTASK_COMPLETED: function (){ return task_child_1.stopped && task_child_2.stopped; }, //All sub task finished
USER_CLICK_STOP: TQ.ON_CLICK('STOP') //User clicked stop button
};
}
function* TaskChild() {
// do something
if(...){
return 'error';
}else{
return 'success';
}
}