Task 单线程多任务框架

多任务问题

对于用户而言, 一个任务中通常包含了若干操作和等待, 以一个简单的止损需求为例:

  • 发出一个买入开仓报单指令
  • 如果报单完全成交, 则: - 等待价格跌破 开仓价 - 30 元, 一旦条件满足, 立即发出一个平仓指令
  • 如果用户撤销了报单, 则任务结束

在这个例子中, 等待发出的开仓指令成交, 和等待价格满足预设条件, 都需要等待一段较长的时间, 在这段时间内, 我们可能还有别的策略需要运行, 因此需要某种多任务机制, 来使多个任务同时执行. 常见的多任务方案有三种

异步回调+状态机模型

CTP API 使用的即是此方式. 每当一个事件发生时, 触发特定的回调函数. 用户在回调函数中编写自己的业务代码对事件进行响应.

Pros:

  • 语法简单, 绝大多数编程语言都直接支持此类模型, 性能较高

Cons:

  • 用户的业务代码被分成两个(或更多)部分, 一部分代码在主线程中执行, 另一部分代码放在回调函数中, 代码结构与需求结构不一致, 导致编码困难
  • 当业务逻辑较复杂时, 需要用户自行构建状态机和管理状态变量
  • 主线程和回调函数线程中的代码如果常常需要访问共同变量, 因此需用户实现转线程或线程锁机制

多线程阻塞模型

每个任务建立一个线程, 在线程中可以方便的执行阻塞和等待.

Pros:

  • 代码结构与需求结构较为接近, 编码较简单
  • 所有业务代码可以组织到一个函数中, 避免状态机和全局变量

Cons:

  • 多线程都对共同数据集执行读写操作, 需要小心的使用锁机制
  • 线程开销较大, 创建大量线程后性能明显下降

基于generator机制的单线程多任务模型

为了克服上面两种机制的困难, 现代编程语言中通常都支持某种形式的 单线程多任务 机制, 例如 golang 中的 coroutines, javascript 和 python 中的 generator 等.

Pros:

  • 代码结构与线程函数相似, 所有业务代码可以组织到一个函数中, 避免状态机和全局变量
  • 代码中明确插入等待事件的代码, 任务管理器只会在这个位置执行任务切换
  • 多任务访问共同变量无需加锁
  • 没有线程开销

Cons:

  • 较老的编程语言对此机制缺乏支持

我们推荐使用这种模型, 并在 TQSDK 中对这种方式给予了专门支持

任务概念

我们将一个任务称为一个 Task. 在实现上, 每个 Task 是一个 Javascript Generator Function.

一个Task的例子
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 后面添加任意条件,等待下单机会。

用 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';
    }
}