NodeJS的异步、并发编程方案

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_36520153/article/details/80443561

一、事件发布/订阅模式(events)

事件发布/订阅模式自身并无同步和异步的问题,但在node中,emit()调用多半是伴随时间循环而异步触发的。
订阅:

emitter.on("event1", function(message){
    console.log(message);
});

发布

emitter.emit("event1","I am message")

注意事项:
- 如果对一个事件添加了超过10个侦听器,将会得到一条警告。因为如果事件相关的监听器过多,可能会导致CPU占用过多。
- 一个健壮的EventEmitter实例应该对error对象做处理

雪崩问题:在高访问量、大并发量的情况下缓存失效的场景,此时大量的请求同时涌入数据库中,数据库无法同时承受如此大的查询请求,进而影响到网站整体的响应速度。

解决方法:引入事件队列

var proxy = new events.EventEmitter();
var status = "ready";
var select = function(callback){
    proxy.once("selected", callback);
    if (status === "ready"){
        status = "spending";
        db.select("SQL", function(results){
            status = "ready";
            callback(results);
        })
    }
}

这里我们利用了once()方法,将所有请求的回调都压入事件队列中,利用其执行一次就会将监听器移除的特点,保证每一次回调只会被执行一次。这样就保证了,对于相同的SQL语句,同一个查询开始到结束的过程永远只有一次。一旦查询结束,得到的结果可以被这些调用者共同使用。
另外,此处可能因为存在监听器躲过而引发警告,需要调用下边的代码移除警告

setMaxListeners(0)

多异步之间的协作方案:利用偏函数来处理哨兵变量和第三方函数的关系

var emitter = new events.Emitter();
var done = after(times, render);

// 多对多
emitter.on("done", done);
emitter.on("done", other);

var after = function(times, callback){
    var count = 0, results = {};
    return function (key, value) {
        results[key] = value;
        count ++;
        if (count === times){
            callback(results);
        }
    }
}

db.query(sql, function(err, data){
    emitter.emit("done", "data", data)
});
fs.readFile(template_path, "utf-8", function(err, template){
    emitter.emit("done", "template", template);
})

EventProxy的 应用

  • all()方法:当all()方法中订阅的多个事件都被触发后,才会执行侦听器。
var ep = new EventProxy();
ep.all("template", "data", function(template, data){
    // TODD
});
fs.readFile(template_path, "utf-8", function(err, template){
    ep.emit("template", template);
})
db.query(sql, function(err, data){
    ep.emit("data", data);
})

注意:all()方法订阅的事件如果没有返回值,则应最后订阅该事件。因为侦听器会按订阅事件的顺序接收返回值。

tail()方法:tail()方法与all()方法的区别在于:all()方法的侦听器在满足条件后只会执行一次,tail()方法的侦听器在满足条件执行一次后,如果组合事件中的某个事件再次触发,侦听器会用最新的数据继续执行。

after()方法:实现事件在执行多少次之后执行侦听器的单一事件组合订阅方式

ep.after("data", 10, function(datas){
    // TODD
})

异常处理:EventProxy提供了fail()和done()这两个实例方法来优化异常处理

ep.fail(callback)

等价于

ep.fail(function(err){
    callback(err);
});

又等价于

ep.bind('error', function(err){
    // 卸掉所有处理函数
    ep.unbind();
    // 异常回调
    callback(err);
});

而done()方法的实现,也可参见如下变换:

ep.done('tp1');

它等价于:

function(err, content){
    if(err){
        return ep.emit('error', err);
    }
    ep.emit('tp1', content);
}

当传递参数为回调函数时,需手动调用emit()触发事件

ep.done(function (content){
    // TODD
    // 无需考虑异常
    ep.emit('tp1', content)
})

改进:同时传入事件名称和回调函数

ep.done('tp1', function(content){
    // TODD
    return content;
})

二、Promise/Deferred模式(Promises/A)

这里写图片描述

让Promise支持链式执行,主要通过以下两个步骤

  1. 将所有的回调都存到队列中
  2. Promise完成时,逐个执行回调,一旦监测到返回了新的Promise对象,停止执行,然后将当前Deffered对象的promies引用改变为新的Promise对象,并将队列中剩余的回调转交给它

实例代码

new Promise(test).then(function (result1) {
    console.log('成功1:' + result1);
}).then(function (result2) {
    console.log('成功2:' + result2);
}).catch(function (reason) {
    console.log('失败:' + reason);
});

推荐廖雪峰的博客

三、流程控制库

尾触发与Next

尾触发目前应用最多的地方是Connect的中间件

var app = connect();
app.use(connect.staticCache());
app.use(connect.static(__dirname + '/public'));
app.use(connect.cookieParser());
app.use(connect.session());
app.use(connect.query());
app.use(connect.bodyParser());
app.use(connect.csrf());
app.listen(3001);

通过use()方法注册好一系列中间件后,监听端口上的请求。中间件利用了尾触发的机制,最简单的中间件如下:

function(req, res, next){
    // 中间件
}

每个中间件传递请求对象、相应对象和尾触发函数,通过队列形成一个处理流
这里写图片描述

async
async提供了series()方法来实现一组任务的串行执行

async.series([
    function(callback){
        fs.readFile('file1.txt', 'utf-8', callback);
    },
    function (callback){
        fs.readFile('file2.txt', 'utf-8', callback);
    }
],
    function(err, results){
    // results => [file1.txt, file2.txt]
})

等价于

fs.readFile('file1.txt', 'utf-8', function (err, content){
    if (err){
        return callback(err);
    }
    fs.readFile('file2.txt', 'utf-8', callback (err, data){
        if (err){
            return callback(err)
        }
        callback( null, [content, data]);
    });
})

callback()并非由指定者指定。每个callback()执行时会将结果保存起来,然后执行下一个调用。一旦出现异常,就结束调用,并将异常传递给最终回调函数的第一个参数。

异步的并行执行:async的parallel()方法

async.parallel([
    function(callback){
        fs.readFile('file1.txt', 'utf-8', callback);
    },
    function (callback){
        fs.readFile('file2.txt', 'utf-8', callback);
    }
],
    function(err, results){
    // results => [file1.txt, file2.txt]
})

异步调用的依赖处理:series()适合五一来的异步串行执行,但当前一个结果是后一个调用的输入时,series()方法就无法满足需求了。async提供了waterfall()方法。

async.waterfall([
    function(callback){
        fs.readFile('file1.txt', 'utf-8', function(err, content){
            callback(err, content);
        })
    },
    function (arg1, callback){
        // arg1 => file2.txt
        fs.readFile(arg1, 'utf-8', function(err, content){
            callback(err, content);
        })
    },
    function (arg2, callback){
        // arg2 => file3.txt
        fs.readFile(arg2, 'utf-8', function(err, content){
            callback(err, content);
        })
    }
],function (err, result){
    // result => file4.txt
})

自动依赖处理:async提供了一个强大的方法auto()实现复杂的业务处理
假设业务场景为:

  1. 从磁盘读取配置文件
  2. 根据配置文件链接MongoDB
  3. 根据配置文件链接Redis
  4. 编译静态文件
  5. 上传静态文件到CDN
  6. 启动服务器

简单映射下上述任务:

{
    readConfig: function(){},
    connectMongoDB: function(){},
    connectRedis: function(){},
    compileAsserts: function(){},
    uploadAsserts: function(){},
    startup: function(){}
}

connectMongoDB和connectRedis依赖readConfig,uploadAsserts依赖compileAsserts,startup依赖所有。

var deps = {
    readConfig : function (callback){
        // read config file
        callback();
    },
    connectMongoDB: ['readConfig', function(callback){
        // connect to mongodb
        callback();
    }],
    connectRedis: ['readConfig', function(callback){
        // connect to redis
        callback();
    }],
    compileAsserts: function(callback){
        // compile asserts
        callback();
    },
    uploadAsserts: ['compileAsserts', function(callback){
        // upload to assert
        callback();
    }],
    startup: ['connectMongoDB', 'connectRedis', 'uploadAsserts', function(callback){
        // startup
    }]
}
async.auto(deps);

用EventProxy实现

ep.asap('readtheconfig', function(){
    // read config file
    ep.emit('readConfig');
}).on('readConfig', function(){
    // connect to mongodb
    ep.emit('connectMongoDB');
}).on('readConfig', function(){
    // connect to redis
    ep.emit('connectRedis');
}).assp('compiletheasserts', function(){
    // compile assert
    ep.emit('compileAsserts');
}).on('compileAsserts', function(){
    // upload to assert
    ep.emit('uploadAsserts');
}).all('connectMongoDB', 'connectRedis', 'compileAsserts', function(){
    // startup
});

Step: 比async更轻量,只有一个接口Step

Step(
    function readFile1(){
        fs.readFile('file1.txt', 'utf-8', this);
    },
    function readFile2(){
        fs.readFile('file2.txt', 'utf-8', this);
    },
    function done(err, content){
        console.log(content);
    }
)

用到了this关键字,是Step内部的一个next()方法,将异步调用的结果传递给下一个任务作为参数,并调用执行。

并行任务执行:Step中可以调用this的parallel()方法,告诉Step需要等所有任务完成时才进行下一个任务

Step(
    function readFile(){
        fs.readFile('file1.txt', 'utf-8', this.parallel());
        fs.readFile('file2.txt', 'utf-8', this.parallel());
    },
    function done(err, content1, content2){
        // content1 => file1
        // content2 => file2
        console.log(arguments);
    }
)

使用parallel()时,如果异步方法的结果返回的是多个参数,Step只会取前两个参数。

结果分组:类似parallel()的效果,但在结果传递上不同:返回值以数据形式返回。主要用于遍历某个方法,然后将结果存入数组中。而parallel()更适合同时运行两个异步函数。

Step(
    function readDir(){
        fs.readdir(__dirname, this);
    },
    function readFile(err, results){
        if (err) throw err;
        // 创建组
        var group = this.group();
        results.forEach(function (filename){
            if (/\.js$/.test(filename)){
                fs.readFile(__dirname + "/" + filename, 'utf-8', group());
            }
        });
    },
    function showAll(err, files){
        if (err) thow err;
        console.dir(files);
    }
)

异步并发控制

bagpipe的解决方案

  1. 通过一个队列来控制并发量
  2. 如果当前活跃的异步调用量小于限定值,从队列中取出执行
  3. 如果活跃调用达到限定值,调用暂时存在队列中
  4. 每个异步调用结束时,从队列中取出新的异步调用执行

bagpipe的API主要暴露了一个push()方法和pull事件,实例代码如下:

var Bagpipe = require('bagpipe');
// 设定最大并发数为10
for (var i = 0; i < 100; i++){
    bagpipe.push(async, function(){
        // 异步回调执行
    })
}
bagpipe.on('full', function(length){
    console.warn("目前队列长度为:", length);
})

拒绝模式:有些场景需要尽快返回数据,快速成功获取数据或者失败,即立即返回数据。这时候可以用bagpipe的拒绝模式。

// 设置最大并发数为10
var bagpipe = new Bagpipe(10, {
    refuse: true
})

在拒绝模式下,如果等待的调用队列也满了之后,新来的调用就直接返回拒绝异常

超时限制:为了防止某些异步调用使用太长的时间,我们需要设置一个时间基线,将那些执行时间太久的异步调用清理出活跃队列,让排队中的异步调用尽快执行,否则在拒绝模式下,会有太多调用因为某个执行的慢,导致得到拒绝异常。

// 设置最大并发数
var bagpipe = new Bagpipe(10, {
    // 设置超时限制为3s
    timeout: 3000
})

——-总结自《深入浅出NodeJS》

ES6中async的用法
async函数返回一个Promise对象,可以使用then方法添加回调函数.当函数执行时,遇到await就会等待其异步操作完成,然后执行函数体后面的语句
1.async放在函数前,表示函数里有异步操作.
如:

async function foo(){} //函数声明const foo = async function foo(){} // 函数表达式

2.async函数的返回值为Promise对象

async function f(){
  return 'hello world';
}
f().then(v => console.log(v));
// "hello world"
async function f(){
  throw new Error('报错了'); // 返回的Promise对象为reject状态
}
f().then(
  v => console.log(v),
  e => console.log(e)
);
// Error: 报错了
// 用catch接受错误信息
async function f(){
  throw new Error('报错了');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error: 报错了

3.await表示紧跟在后面的表达式需要等待结果.一般为Promise对象,如果不是,会被转成一个立即resolve的Promise对象
4. 防止异步函数出错,需要把await命令放在try…catch代码块中.和for循环一起用可实现重复尝试某函数.如网络原因可能发送邮件失败,尝试多次发送,捕捉失败原因,一旦成功,break结束循环.

function test2(){
  setTimeout(()=>{
    console.log(1000);
  },1000)
}

async function test(){
  for(let i = 0; i < 5; i++){
    try {
      await test2(); // 异步函数
      break

    } catch (e) {
      console.log('a', e);
    }
  }

}
test();

猜你喜欢

转载自blog.csdn.net/qq_36520153/article/details/80443561