为AJAX实现互斥

  随着AJAX范例得到越来越广泛的应用,浏览器页面可以在向后台服务器请求数据的同时保持前端用户界面的活跃性(因此在AJAX中称为异步)。然而,当这两个活动同时访问共用的JavaScript和DOM数据结构时就会引发问题。JavaScript没有提供针对该并发程序问题的经典解决方案。本文描述了作者在互斥机制方面的新见解,该经过验证的互斥机制在JavaScript中能发挥良好的作用。

为什么需要互斥?

  当多个程序逻辑线程同时访问相同数据的时候,问题便产生了。程序通常假定与其交互的数据在交互过程中不发生改变。访问这些共享数据结构的代码称为临界区,一次只允许一个程序访问的机制被称为互斥。在AJAX应用程序中,当对来自XMLHttpRequest的应答进行异步处理的代码同时操纵正在被用户界面使用的数据时,便会发生这种情况。这个共用的数据可能是用于实现MVC数据模型的JavaScript和/或web页面自身的DOM。如果二者中的任一个对共享数据做了不协调的更改,那么二者的逻辑都将中断。

  也许您会说“等等,为什么我没有遇到过这种问题?”。遗憾的是,这种问题是同步依赖的(也叫做竞态条件),因此它们并不总是发生,或者也许从不发生。它们的或然性基于许多因素。基于健壮性考虑,富internet应用程序应该通过确保这些问题不会发生来阻止出现这种情况。

  因此,需要一种互斥机制来确保同时只能打开一个临界区,并且在它结束之后才能打开另一个。在大多数主流计算机语言和执行框架中,都提供互斥机制(经常是几种),但是应用于浏览器端的JavaScript却没有提供这种互斥机制。虽然存在一些无需专门的语言或环境支持的经典互斥实现算法,但是即使这样还是需要一些JavaScript和浏览器(如Internet Explorer)所缺少的要素。接下来介绍的经典算法在这些浏览器和语言中能发挥良好的作用。

面包店算法

  在计算机科学文献中的几种互斥算法中,所谓的Lamport面包店算法可以有效地用于多个相互竞争的控制线程,该算法中线程之间的通信只能在共享内存中进行(即,不需要诸如信号量、原子性的set-and-test之类的专门机制)。该算法的基本思想源于面包店,因为面包店需要先取号然后等候叫号。清单1给出了该算法的框架(引自Wikipedia),该算法可以使各线程进出临界区而不产生冲突。

清单1. Lamport面包店算法伪代码

// declaration & initial values of global variables
Enter, Number: array [1..N] of integer = {0};

// logic used by each thread...
// where "(a, b) < (c, d)"
// means "(a < c) or ((a == c) and (b < d))"
Thread(i) {
  while (true) {
    Enter [i] = 1;
    Number[i] = 1 + max(Number[1],...,Number[N]);
    Enter [i] = 0;
    for (j=1; j<=N; ++j) {
      while (Enter[j] != 0) {
        // wait until thread j receives its number
      }
      while ((Number[j]!=0)
         && ((Number[j],j) < (Number[i],i))) {
        // wait until threads with smaller numbers
        // or with the same number, but with higher
        // priority, finish their work
      }
    }
    // critical section...
    Number[i] = 0;
    // non-critical section...
  }
}

  如上所示,该算法假定各线程清楚自己的线程编号(常量i)和当前正在活动的线程总数(常量N)。此外,还假定存在一种等待或休眠方式,例如:暂时将CPU释放给其他线程。遗憾的是,Internet Explorer中的JavaScript没有这种能力。虽然如此,如果实际运行在同一线程上的多个代码部分表现为各自运行在独立的虚拟线程上,那么该面包店算法不会中断。同样,JavaScript具有一种在指定延迟后调度函数的机制,所以,可以使用下面的这些方法来优化面包店算法。

Wallace变体

  在JavaScript中实现Lamport面包店算法的主要障碍在于缺少线程API。无法确定当前正在哪个线程上运行以及当前正在活动的线程数目,也无法将CPU释放给其他的线程,无法创建新的线程来管理其他线程。因此,无法查证如何将特定的浏览器事件(例如:单击按纽、可用的XML应答等)分配到线程。

  克服这些障碍的一种方法是使用Command设计模式。通过将所有应该进入临界区的逻辑以及所有启动该逻辑所需的数据一起放入到command 对象中,可以在负责管理command的类中重写面包店算法。该互斥类仅在没有其他临界区(封装为独立的command对象方法)在执行时调用临界区,就像它们各自运行在不同的虚拟线程中一样。JavaScript的setTimeout()机制用于将CPU释放给其他正在等待的command。

  为command对象假定一个简单的基类(见清单2中的Command),可以定义一个类(见清单3中的Mutex)来实现面包店算法的Wallace变体。注意,虽然可以通过很多方式在JavaScript中实现基类对象(为了简洁起见,这里使用一种简单的方式),但是只要各个command对象拥有某个惟一的id,而且整个临界区被封装在单独的方法中,那么任何对象模式都可以使用这种方法。

清单2. 用于 Command 对象的简单基类

 1 function Command() {
 2  if (!Command.NextID) Command.NextID = 0;
 3  this.id = ++Command.NextID;
 4  // unsynchronized API
 5  this.doit = function(){ alert("DOIT called"); }
 6  this.undo = function(){ alert("UNDO called"); }
 7  this.redo = function(){ this.doit();          }
 8  // synchronized API
 9  this.sDoIt = function(){ new Mutex(this,"doit"); }
10  this.sUnDo = function(){ new Mutex(this,"undo"); }
11  this.sReDo = function(){ new Mutex(this,"redo"); }
12 }

  Command类演示了三个临界区方法(见5-7行),但是只要预先将对该方法的调用封装在Mutex中(见9-11行),那么就可以使用任何方法。有必要认识到,常规方法调用(例如非同步的方法调用)与同步方法调用之间存在着重要的区别:具有讽刺意味的是,必须保证同步方法不同步运行。换句话说,当调用sDoIt()方法时,必须确保方法doit()还未运行,即使方法sDoIt()已经返回。doit()方法可能已结束,或者直到将来的某一时间才开始执行。也就是说,将对Mutex的实例化视为启动一个新的线程。

清单3.作为类 Mutex实现的 Wallace 变体

 1 function Mutex( cmdObject, methodName ) {
 2   // define static field and method
 3   if (!Mutex.Wait) Mutex.Wait = new Map();
 4   Mutex.SLICE = function( cmdID, startID ) {
 5     Mutex.Wait.get(cmdID).attempt( Mutex.Wait.get(startID) );
 6   }
 7   // define instance method
 8   this.attempt = function( start ) {
 9     for (var j=start; j; j=Mutex.Wait.next(j.c.id)) {
10       if (j.enter
11       || (j.number && (j.number < this.number ||
12                       (j.number == this.number
13                        && j.c.id < this.c.id))))
14        return setTimeout
15  ("Mutex.SLICE("+this.c.id+","+j.c.id+")",10);
16     }
17     //run with exclusive access
18     this.c[ this.methodID ]();
19     //release exclusive access
20     this.number = 0;
21     Mutex.Wait.remove( this.c.id );
22   }
23   // constructor logic
24   this.c        = cmdObject;
25   this.methodID = methodName;
26   //(enter and number are "false" here)
27   Mutex.Wait.add( this.c.id, this );
28   this.enter    = true;
29   this.number   = (new Date()).getTime();
30   this.enter    = false;
31   this.attempt( Mutex.Wait.first() );
32 }

  Mutex类的基本逻辑是将每个新的Mutex实例放入主等待清单,然后将其在等待队列中启动。因为每次到达“队首”的尝试都需要等待(除了最后一次),所以使用setTimeout来调度每次在当前尝试停止的位置启动的新尝试。到达队首时(见17行),便实现了互斥性访问;因此,可以调用临界区方法。执行完临界区后,释放互斥性访问并从等待清单中移除Mutex实例(见20-21行)。

  Mutex构造函数(见23-31行)记录其Command对象和方法名参数,然后寄存在一个运行中临界区的稀疏数组中(Mutex.Wait),这通过清单4中所示的Map类来实现。然后构造函数获得下一个编号,并在队尾开始排队。由于等待编号中的间隔或副本不存在问题,所以实际上使用当前的时间戳作为下一个编号。

  attempt()方法将初始伪代码中的两个wait循环组合成一个单独的循环,该循环直到队首时才对临界区失效。该循环是一种忙碌-等待循环检测方式,可以通过在setTimeout()调用中指定延迟量来终止该循环。由于setTimeout需要调用“无格式函数”,所以在第4-6行定义了静态帮助器方法(Mutex.SLICE)。SLICE在主等待清单中查找指定的Mutex对象,然后调用其attempt()方法,用start参数指定到目前为止其所获得的等待清单的长度。每次SLICE()调用都像获得了“一块CPU”。这种(通过setTimeout)适时释放CPU的协作方式令人想到协同程序。

清单4. 作为 Map数据结构实现的稀疏数组

function Map() {
  this.map  = new Object();
  // Map API
  this.add = function( k,o ){
    this.map[k] = o;
  }
  this.remove = function( k ){
    delete this.map[k];
  }
  this.get = function( k ){
    return k==null ? null : this.map[k];
  }
  this.first = function(){
    return this.get( this.nextKey() );
  }
  this.next = function( k ){
    return this.get( this.nextKey(k) );
  }
  this.nextKey = function( k ){
    for (i in this.map) {
      if ( !k ) return i;
      if (k==i) k=null; /*tricky*/
    }
    return null;
  }
}

富Internet应用程序集成

  由于Mutex所处理的线程(虚拟的或者非虚拟的)数量是动态变化的,所以可以确定一个基本事实:无法通过像浏览器为各个浏览器事件分配单独的线程那样的方式来获得线程标识符。这里做了一个类似的假定,那就是每个完整的事件处理程序组成一个完整的临界区。基于这些假定,每个事件处理函数都可以转变成一个command对象,并使用Mutex对其进行管理。当然,如果未将代码明确组织成事件处理函数,那么将需要重构。换句话说,不是直接在HTML事件属性中进行逻辑编码(例如:οnclick='++var'),而是调用事件处理函数(例如:οnclick='FOO()'和function FOO(){++var;})。

清单5. 使用了非同步事件处理程序的示例web页面

<html>
<script language="JavaScript">
  function newState(){
    if (XMLreq.readyState==4) processReply();
  }
  function requestData(){
      ...set up asynchronous XML request...
      XMLreq.onreadystatechange = newState;
      ...launch XML request...
  }
  function processReply(){
      var transformedData = ...process data to HTML...
      OutputArea.innerHTML = transformedData + "<br>";
  }
  function clearArea(){
      OutputArea.innerHTML = "cleared<br>"; 
  }
</script>
<body οnlοad="requestData();">
 <input type="button" value="clear" οnclick="clearArea()">
 <div id="OutputArea"/>
</body>
</html>

  例如,假设有三个事件处理程序函数,它们操纵清单5所示的共用数据。它们处理页面加载事件、单击按钮事件和来自XML请求的应答事件。页面加载事件发出某个异步请求来要求获取数据并指定请求-应答事件处理程序,该处理程序处理接收到的数据,并将其加载到共用数据结构。单击按钮事件处理程序也影响共用数据结构。为了避免这些事件处理程序发生冲突,可以通过清单6所示的Mutex将它们转变成command并加以调用(假设JavaScript include文件mutex.js中包含Map和Mutex)。注意,虽然可以使用优美的类继承机制来实现Command子类,但是该代码说明了最简单的方法,该方法仅需要全局变量NEXT_CMD_ID。

清单6. 转化为同步事件处理程序的web页面

<html>
<script src="mutex.js"></script>
<script language="JavaScript">
  function requestData (){
    new Mutex(new  RequestDataCmd(),"go"); }
  function processReply(){
    new Mutex(new ProcessReplyCmd(),"go"); }
  function clearArea   (){
    new Mutex(new    ClearAreaCmd(),"go"); }
  function newState    (){
    if (XMLreq.readyState==4) processReply(); }

  var NEXT_CMD_ID = 0;

  function RequestDataCmd(){
    this.id = ++NEXT_CMD_ID;
    this.go = function(){
      ...set up asynchronous XML request...
      XMLreq.onreadystatechange = NewState;
      ...launch XML request...
    }
  }
  function ProcessReplyCmd(){
    this.id = ++NEXT_CMD_ID;
    this.go = function(){
      var transformedData = ...process data to HTML...
      OutputArea.innerHTML = transformedData + "<br>";
    }
  }
  function ClearAreaCmd(){
    this.id = ++NEXT_CMD_ID;
    this.go = function(){ 
      OutputArea.innerHTML = "cleared<br>"; }
  }
</script>
<body οnlοad="requestData();">
 <input type="button" value="clear" οnclick="clearArea()">
 <div id="OutputArea"/>
</body>
</html>

  已经通过Mutex将这三个事件处理程序函数转变为调用它们的初始逻辑(当前都被预包装于command类中)。各个command类定义一个独特的标识符和一个包含临界区逻辑的方法,从而满足了command接口的要求。

结束语

  借助于AJAX和RIA,构建复杂的动态用户界面的推动力正在促使开发人员使用先前与胖GUI客户端紧密联系的设计模式(例如:模型-视图-控制器)。随着视图和控制器的定义模块化,且每一个都带有自己的事件和事件处理程序(除了共用数据模型),发生冲突的机率成倍提高。通过把事件处理逻辑封装到Command类中,不仅可以使用Wallace变体,而且为提供丰富的撤消/重做功能、脚本编写界面和单元测试工具创造了条件。

参考资料

  • 本文的示例代码可供浏览或下载。它包括了一些在本文中被省略的细节,比如web页面可以无需服务器连接而直接在浏览器中执行。
  • 有一个可供浏览或下载的JavaScript示例框架(Gravy),它使用了本文中所涉及的技术,同时包括JsDoc文档。Gravy支持将所有浏览器中的应用程序功能以JavaScript方式实现。应用程序仅需要访问服务器来执行数据库CRUD操作 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Verilog 中可以使用锁、FIFO 等方式来实现互斥机制。下面是一个使用锁(mutex)实现互斥机制的例子。 ```verilog module mutex ( input clk, input rst, input req, output reg grant ); reg locked; // 锁的状态 reg request_lock; // 请求锁 reg release_lock; // 释放锁 always @(posedge clk) begin if (rst) begin locked <= 0; request_lock <= 0; release_lock <= 0; grant <= 0; end else begin // 处理请求锁的信号 if (req && !locked) begin request_lock <= 1; end else begin request_lock <= 0; end // 处理释放锁的信号 if (locked) begin release_lock <= 1; end else begin release_lock <= 0; end // 如果锁已经被占用,则不授予资源 if (locked) begin grant <= 0; end else begin grant <= 1; end // 处理锁的状态 if (request_lock && !locked) begin locked <= 1; end else if (release_lock && locked) begin locked <= 0; end end end endmodule ``` 在上面的代码中,我们使用一个名为 `locked` 的寄存器来表示锁的状态,初始值为 0 表示锁未被占用。`request_lock` 和 `release_lock` 分别表示请求锁和释放锁的信号。 在时钟上升沿时,根据输入的复位信号 `rst`、请求信号 `req`,和锁的状态来判断是否授予资源,并更新锁的状态。 需要注意的是,上面的代码只是一种简单的实现方式,可能存在死锁等问题。在实际应用中,需要根据具体的需求和系统环境来选择适当的实现方式,以保证系统的正确性和性能。 ### 回答2: Verilog是一种硬件描述语言,用于描述数字电路和系统级的数字系统。要实现互斥机制,可以使用Verilog编写一个互斥模块,该模块基于锁(lock)来控制对共享资源的访问。 互斥机制将保证在任何给定时间只有一个模块可以访问共享资源。下面是一个简单的Verilog模块的示例,使用了一把互斥锁: ```verilog module Mutex( input clk, input reset, input request, output reg grant ); reg locked; always @(posedge clk or posedge reset) begin if(reset) begin locked <= 0; grant <= 0; end else if(request && !locked) begin locked <= 1; grant <= 1; end else begin locked <= locked; grant <= 0; end end endmodule ``` 在上述代码中,互斥模块包含一个时钟信号`clk`和一个复位信号`reset`,以及一个申请信号`request`和一个授权信号`grant`。当申请信号为1且锁为0时,表示申请获得了互斥锁,互斥模块将锁定,并将授权信号设置为1。否则,授权信号将保持为0。 在应用中,其他模块在需要访问共享资源之前,首先向互斥模块发送申请信号。如果收到了授权信号,表示已经获得了互斥锁,可以访问共享资源。否则,需要等待其他模块释放互斥锁后再次尝试。 这只是一个简单的互斥机制示例,实际应用中可能需要更复杂的实现。不过,这个简单的Verilog实现可以作为一个基本的理解和起点。 ### 回答3: Verilog是一种硬件描述语言,用于设计和验证数字电路。实现互斥机制需要使用互斥锁或信号量来保护共享资源,以防止多个处理器或线程同时访问该资源。 在Verilog中,可以使用互斥锁原语来实现互斥机制。互斥锁原语包括`mutex`, `try_mutex`, `ior_mutex`和`fifo_mutex`。这些互斥锁原语可以用来实现互斥访问共享资源的阻塞或非阻塞模式。 互斥锁的基本原理是,当一个处理器或线程需要访问共享资源时,它会先尝试获取互斥锁。如果互斥锁已被其他处理器或线程获取,则请求者将被阻塞,直到互斥锁被释放。一旦互斥锁被释放,请求者将获取互斥锁以访问该资源。 下面是一个用Verilog实现互斥机制的简单示例代码: ```verilog module mutex_example; reg [1:0] mutex; wire [1:0] request; always @(posedge clk) begin if (request) begin if (mutex == 2'b00) begin mutex <= 2'b01; // 这里为了简化示例,假设有一个共享资源需要访问 // 可在这里进行对共享资源的操作 mutex <= 2'b00; // 访问结束后释放互斥锁 end end end initial begin mutex <= 2'b00; // 初始化互斥锁为未被获取状态 #10 request <= 1; // 发送资源请求 #10 request <= 0; // 停止发送资源请求 #10 $finish; // 停止仿真 end endmodule ``` 在这个例子中,我们使用了一个`mutex`寄存器来表示互斥锁的状态,其中`00`表示未被获取状态,`01`表示正在获取且未被其他请求者阻塞,`10`表示已被其他请求者获取。我们还使用了一个`request`线作为资源请求信号。在仿真开始后的10个时间单位,我们将`request`信号置为高电平,表示发送资源请求。如果互斥锁未被获取,则我们获取互斥锁并访问共享资源,然后释放互斥锁。最后,我们停止发送资源请求并结束仿真。 这只是一个简单的示例,实际的互斥机制的实现可能更加复杂,并包括更多的约束和逻辑。但是,通过使用互斥锁原语,我们可以在Verilog中实现互斥机制,以保护共享资源的安全访问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值