展开目录
异步并发也要保证原子性
nodejs异步原子性
X
陈尼玛的博客
记录开发生涯的踩坑经历,用时间来验证成长
加载中

需要用nodejs实现一个临时文件的缓存模块。因为缓存的文件名是用一个自动增长的数字表示的,如果缓存文件的到期时间较短,那么到期之后删掉这个文件就可以继续使用这个名字了。但也可能某个缓存的时间很长,持久的占用这个名字。所以必须要在写进缓存文件之前判断这个缓存文件名是不是存在。

  1. 判断是否存在
  2. 创建文件
  3. 返回文件句柄

把这三个步骤封装一下可以得到以下方法:

var fs=require('fs');

// 方便起见我把stat,open都转成Promise的形式,否则写起来太乱了
['stat','open'].map(function(h){
  fs[h+'Then']=function(){
    var a=Array.prototype.slice.call(arguments);
    return new Promise(function(resolve,reject){
      a.push(function(e){
        e?reject(e):resolve(Array.prototype.slice.call(arguments,1));
      });
      fs[h].apply(fs,a);
    });
  };
});

function getTempHandleThen(i){
  return new Promise(function(resolve,reject){
    fs.statThen(i).then(
      function(){
        reject({err:'already exists'});
      },function(e){
        if(e.errno!==-4058)return reject(e);
        return fs.openThen(i,'w');
      }
    ).then(
      function(args){
        resolve(args[0]);
      },reject
    );
  });
}

理想的调用情况是这样的:

getTempHandleThen('test0').then(function(fd){
  console.log('success',fd);
},function(e){
  console.log('fail',e);
});

如果test0不存在,则会得到success的结果,否则是fail。 但是在并发的情况下,会得到一个错误的结果:

// 模拟10并发
for(var i=0;i<10;i++)
getTempHandleThen('test'+i%5).then(function(fd){
  console.log('success',fd);
},function(e){
  console.log('fail',e);
});

运行结果:

<center> 错误 </center>

如果一开始test0不存在的话,会得到10个success的结果。产生这种错误结果的原因是:这三个步骤异步进行,所以多个并行的回调会在第一步同时认为某一个文件不存在,然后对同一个文件进行写入。

同步阻塞可以解决问题,但是在单线程的js程序中这么做简直作死。所以解决办法必然是异步的,不难想到加锁然后以顺序队列执行这个办法,实现并不困难。真正的问题在于这个办法的api应该如何设计,必须调用简单,而且通用性强。根据以往的经验,我认为在外部包装一层函数是一个合适的办法。所以最后实现了这样代码:

function addLock(fn){
  // lock是一个hash表,键名为锁的关键字,值为Promise队列。
  // lock保证了同一个关键字的任务会添加到队列尾部,而不是立即执行。
  var lock={};

  // count也是一个hash表,键名同上,值为Promise队列中未执行任务的长度。
  // count是为了当长度为0时对这个队列的引用全部删除,便于垃圾回收。
  var count={};

  // 返回的是一个新的函数,第一个参数为锁的关键字,第二个参数为调用原始函数时用到的参数数组
  return function(lockid,args){
    var dojo=function(){ return fn.apply(this,args); };
    var clean=function(){
      if(--count[lockid])return;
      delete(lock[lockid]);
      delete(count[lockid]);
    };
    lock[lockid]=lock[lockid]||Promise.resolve();
    count[lockid]=count[lockid]||0;
    count[lockid]++;
    // 执行原始任务
    lock[lockid]=lock[lockid].then(dojo,dojo);
    // 如果这个锁下所有的任务都完成了,那么从lock中删掉引用
    lock[lockid].then(clean,clean);
    // 返回值和原始函数的返回值保持一致
    return lock[lockid];
  };
}

于是把上面的getTempHandleThen用addLock包装,在执行可以得到正确的结果了:


// 包装成可加锁的函数
_getTempHandleThen=addLock(getTempHandleThen);

// 模拟10并发
for(var i=0;i<10;i++)
// 用i%5作为锁的关键字
_getTempHandleThen(i%5,['test'+i%5]).then(function(fd){
  console.log('success',fd);
},function(e){
  console.log('fail',e);
});

相关文档

暂无

随便看看

  1. windows电脑防止自动休眠

  2. git 大小写不区分问题

  3. cnpm 立即同步

  4. 安卓文字偏上,文字顶部被遮罩

  5. ipsec vpn 添加新账号

  6. css3 文字渐变色

  7. nodejs俄罗斯方块

  8. 单页应用的单向数据流的流程图

  9. git记住/删除账号密码

  10. git 设置代理服务器

  11. 树莓派配置wifi

  12. mysql选取内容导出到文件

  13. replace2正则扩展方法

  14. 猴子选大王算法问题

  15. 数据库清理优化

  16. 配置mysql ssl连接

畅言模块加载中