用户在非阻塞单线程环境中进行交互式解释(棘手)

时间:2010-11-22 10:52:57

标签: javascript node.js interpreter nonblocking

对于学校项目,我和同学正在javascript(节点)中编写特定于域的语言。该语言包含需要通过Websocket连接进行用户输入的语句。

当语句需要用户输入时,解释器必须停止执行,并等待事件。

通常会暂停线程并在继续线程之前等待接收用户输入,但我们不能这样做,因为node.js是单线程的,并且在不阻塞处理器的情况下不提供睡眠选项。

我们尝试了很多方法来解决它,但失败了。 : - (

这个问题的答案可能是如何建立一个可以解释的翻译的建议。

下面,我们将完成解释器的简化(有错误)

我们用这些节点构建一个抽象语法树

var Print = function( str ){
  this.str = str;
}
var Block = function( stats ){
  this.stats = stats;
}
var Delayed = function( stats ){
  this.stats = stats;
}
var Loop = function( times, stats ){
  this.times = times;
  this.stats = stats;
}
  • 打印 - 一个简单的陈述,永远不需要暂停。
  • 阻止 - 一系列陈述
  • 延迟 - 一段时间后要执行的一系列语句。
  • 循环 - 一系列陈述的多次迭代

树看起来像这样:

var ast = new Block([
  new Delayed([
    new Print("blah blah"),
    new Delayed([])
  ]),
  new Loop(3,[
    new Delayed([
      new Print("loop delayed")
    ])
  ])
]);

用于评估陈述的解释器。请注意,此代码不能正常工作。它永远不会停下来等待输入。

var Interpreter = function( ast ){
  this.ast = ast;
}

Interpreter.prototype.run = function(){
  this.handle( this.ast );
}

Interpreter.prototype.handleAll = function( stats ){
  for( var i = 0; i < stats.length; i++ ){
    this.handle(stats[i]);
  }
}

Interpreter.prototype.handle = function( stat ){
  var t = this;
  /*-----------------------------------------------*
   *   Simple statement - no need for pause here   *
   *-----------------------------------------------*/
  if( stat instanceof Print ){
    sys.puts(stat.str);
  }

  /*-----------------------------------------------------*
   *   Delayed - this might contain more delayed stats   *
   *-----------------------------------------------------*/
  else if( stat instanceof Delayed ){
    sys.debug("waiting for user input");
    // this represents a user input with a string
    setTimeout(function(str){

      sys.debug("done waiting");
      sys.puts(str);

      // this might contain delayed stats 
      t.handleAll(stat.stats);

    }, 2000, "some string");
  }

  // ============================================
  // = Block - this might contain delayed stats =
  // ============================================
  else if( stat instanceof Block ){
    sys.debug("doing a block - before");

    this.handleAll(stat.stats);

    sys.debug("doing a block - after");
  }


  // ===========================================
  // = Loop - this might contain delayed stats =
  // ===========================================
  else if( stat instanceof Loop ){
    sys.debug("before loop");
    for( var i = 0; i < stat.times; i++ ){
      sys.debug("inside loop[" + i + "] - begin");

      // this will maybe contain delayed stats
      this.handleAll(stat.stats); 

      sys.debug("inside loop[" + i + "] - end");
    }
    sys.debug("after loop");
  }

  else {
    throw "error.. statement not recognized"
  }
}

解释器需要在遇到“延迟”语句时暂停,然后在延迟完成时继续。

上面的代码永远不会停顿。遇到“延迟”语句时,子语句会延迟,但执行“延迟”后的其他语句。

对于代码的非碎片版本,请{se http://pastie.org/1317023

2 个答案:

答案 0 :(得分:1)

我认为Ivo的答案基本上是正确的,但我会尝试重新措辞并添加一些建议:

  1. Delayed()应该是一个离开动作,而不是AST的内部节点 - 假设我得到了正确的语义:它应该阻塞直到收到数据,然后完成/终止。

    < / LI>
  2. 你需要模仿程序计数器和堆栈帧的概念。对于真正构造的动作(例如循环),堆栈帧需要包含循环变量的当前值以及语句序列中的当前位置。您不应该为该状态回收AST对象,因为同一个循环可能同时执行多次(假设有多个客户端)。

  3. 状态有“下一步”操作,执行一个步骤。首次调用时,延迟会立即返回一个代码,表明不需要进一步执行。第二次调用时,它什么都不做(表示操作已完成)。

答案 1 :(得分:0)

你的循环是完全错误的。而不是在这里使用循环:

for( var i = 0; i < stat.times; i++ ){

}

您需要修改完整的句柄功能:

// you might want to make it so that you can pass null to indicate blocking etc.
Interpreter.prototype.handle = function( stat ){ 
    var that = this;
    var wait = 0;

    // in case of delayed, just set wait to the desired delay

    // in case of a loop, well you either go recursive or use a stack based approach

    // fake the loop
    setTimeout(function(){that.handle();}, wait); 
}

所以你需要通过回调“伪造”你的循环,看起来很棘手,但它确实不具备循环的所有优点(你需要我上面提到的堆栈/递归),但它也给出了你想要的所有其他东西。

对于WebSocket输入,也是异步,在数据事件中,您只需检查当前是否阻止,如果是,则将数据作为用户输入提供。

请记住,一次只运行一件事,所以如果你遍历你的程序,没有其他任何东西都有机会运行,即使你的WebSocket事件只会排队,然后在之后触发所有你的循环结束了。