响应式编程-异步编程的问题与困难
ReactiveX is a library for composing asynchronous and event-based programs by using observable sequences.
问题:1、回调地狱;2、逻辑分散;
传统方案:回调;
改进方案:promise;
改进方案:monad;
改进方案:rx;
异步编程的挑战
异步编程的主要困难在于,构建程序的执行逻辑时是非线性的,这需要将任务流分解成很多小的步骤,再通过异步回调函数的形式组合起来。在异步大行其道的javascript
界经常可以看到很多层的});
,简单酸爽到妙不可言。这一节将讨论一些常用的处理异步的技术手段。
回调函数地狱
开头的那个例子使用了4层的嵌套回调函数,如果流程更加复杂的话,还需要嵌套更多,这不是一个好的实践。而且以回调的方式组织流程,在视觉上并不是很直白,我们需要更加优雅的方式来解耦和组织异步流。
使用传统的javascript
技术,可以展平回调层次,例如我们可以改写之前的例子:
var ohs = require('./anticorruption/OpenHostService');
var localConvertingService = require('./services/LocalConverting');
var remoteRepository = require('./repositories/BusinessData');
var calculationService = require('./services/Calculation');
function(req, res) {
var userData = req.body;
ohs.retrieveResource(userData, ohsCb);
function ohsCb(err, rs1) {
if(err) {
// error handling
}
localConvertingService.unitize(rs1, convertingCb);
}
function convertingCb(err, rs2) {
if(err) {
// error handling
}
remoteRepository.loadBusinessData(rs2, loadDataCb);
}
function loadDataCb(err, bs1) {
if(err) {
// error handling
}
calculationService.doCalculation(bs1 , calclationCb);
}
function calclationCb(err, result) {
if(err) {
// error handling
}
res.view(result);
}
}
解嵌套的关键在于如何处理函数作用域,之后金字塔厄运迎刃而解。
还有一种更为优雅的javascript
回调函数处理方式,可以参考后面的Promise
部分。
而对于像C#
之类的内建异步支持的语言,那么上述问题更加的不是问题,例如:
public async IActionResult CrazyCase(UserData userData) {
var ticket = CrazyApplication.Ticket;
var ohsFactory = new OpenHostServiceFactory(ticket);
var ohs = ohsFactory.CreateService();
var ohsAdapter = new OhsAdapter(userData);
var rs1 = await ohs.RetrieveResource(ohsAdapter);
var rs2 = await _localConvertingService.Unitize(rs1);
var bs1 = await _remoteRepository.LoadBusinessData(rs2);
var result = await _calculationService.DoCalculation(bs1);
return View(result);
}
async/await
这糖简直不能更甜了,其它C#
的编译器还是生成了使用TPL
特性的代码来做异步,说白了就是一些Task<T>
在做后台的任务,当遇到async/await
关键字后,编译器将该方法编译为状态机,所以该方法就可以在await
的地方挂起和恢复了。整个的开发体验几乎完全是同步式的思维在做异步的事儿。后面有关于TPL
的简单介绍。
异常处理
由于异步执行采用非阻塞的方式,所以当前的执行线程在调用后捕获不到异步执行栈,因此传统的异步处理将不再适用。举两个例子:
try {
Task.Factory.StartNew(() => {
throw new InvalidOperationException("diablo coming.");
});
} catch(InvalidOperationException e) {
// nothing captured.
throw;
}
try {
process.nextTick(function() {
throw new Error('diablo coming.');
});
} catch(e) {
// nothing captured.
throw e;
}
在这两个例子中,try
语句块中的调用会立即返回,不会触发catch
语句。那么如何在异步中处理异常呢?我们考虑异步执行结束后会触发回调函数,那么这便是处理异常的最佳地点。node
的回调函数几乎总是接受一个错误作为其首个参数,例如:
fs.readFile('file.txt', 'utf-8', function(err, data) { });
其中的err
是由异步任务本身产生的,这是一种自然的处理异步异常的方式。那么回到C#
中,因为有了好用的async/await
,我们可以使用同步式的思维来处理异常:
try {
await Task.Factory.StartNew(() => {
throw new InvalidOperationException("diablo coming.");
});
} catch(InvalidOperationException e) {
// exception handling.
}
编译器所构建的状态机可以支持异常的处理,简直是强大到无与伦比。当然,对于TPL
的处理也有其专属的支持,类似于node
的处理方式:
Task.Factory.StartNew(() => {
throw new InvalidOperationException("diablo coming.");
})
.ContinueWith(parent => {
var parentException = parent.Exception;
});
注意这里访问到的parent.Exception
是一个AggregateException
类型,对应的处理方式也较传统的异常处理也稍有不同:
parentException.Handle(e => {
if(e is InvalidOperationException) {
// exception handling.
return true;
}
return false;
});
异步流程控制
异步的技术也许明白了,但是遇到更复杂的异步场景呢?假设我们需要异步并行的将目录下的3个文件读出,全部完成后进行内容拼接,那么就需要更细粒度的流程控制。
我们可以借鉴async.js这款优秀的异步流程控制库所带来的便捷。
async.parallel([
function(callback) {
fs.readFile('f1.txt', 'utf-8', callback)
},
function(callback) {
fs.readFile('f2.txt', 'utf-8', callback)
},
function(callback) {
fs.readFile('f3.txt', 'utf-8', callback)
}
], function (err, fileResults) {
// concat the content of each files
});
如果使用C#
并配合TPL
,那么这个场景可以这么实现:
public async void AsyncDemo() {
var files = new []{
"f1.txt",
"f2.txt",
"f3.txt"
};
var tasks = files.Select(file => {
return Task.Factory.StartNew(() => {
return File.ReadAllText(file);
});
});
await Task.WhenAll(tasks);
var fileContents = tasks.Select(t => t.Result);
// concat the content of each files
}
我们再回到我们开头遇到到的那个场景,可以使用async.js
的waterfall
来简化:
var ohs = require('./anticorruption/OpenHostService');
var localConvertingService = require('./services/LocalConverting');
var remoteRepository = require('./repositories/BusinessData');
var calculationService = require('./services/Calculation');
var async = require('async');
function(req, res) {
var userData = req.body;
async.waterfall([
function(callback) {
ohs.retrieveResource(userData, function(err, rs1) {
callback(err, rs1);
});
},
function(rs1, callback) {
localConvertingService.unitize(rs1, function(err, rs2) {
callback(err, rs2);
});
},
function(rs2, callback) {
remoteRepository.loadBusinessData(rs2, function(err, bs1) {
callback(err, bs1);
});
},
function(bs1, callback) {
calculationService.doCalculation(bs1, function(err, result) {
callback(err, result);
});
}
],
function(err, result) {
if(err) {
// error handling
}
res.view(result);
});
}
如果需要处理前后无依赖的异步任务流可以使用async.series()
来串行异步任务,例如先开电源再开热水器电源最后亮起红灯,并没有数据的依赖,但有先后的顺序。用法和之前的parallel()
及waterfall()
大同小异。另外还有优秀的轻量级方案step,以及为javascript
提供monadic扩展的wind.js(特别像C#
提供的方案),有兴趣可以深入了解。
反人类的编程思维
异步是反人类的
人类生活在一个充满异步事件的世界,但是开发者在构建应用时却遵循同步式思维,究其原因就是因为同步符合直觉,并且可以简化应用程序的构建。
究其深层原因,就是因为现实生活中我们是在演绎,并通过不同的口头回调
来完成一系列的异步任务,我们会说你要是有空了来找我聊人生,货到了给我打电话,小红你写完文案了交给小明,小丽等所有的钱都到了通知小强……而在做开发时,我们是在列清单,我们的说法就是:我等着你有空然后开始聊人生,我等着货到了然后我就知道了,我等着小红文案写完了然后开始让她交给小明,我等着小丽确认所有的钱到了然后开始让她通知小强……
同步的思维可以简化编程的关注点,但是没有将流程进行现实化的切分,我们总是倾向于用同步阻塞的方式来将开发变成简单的步骤程序化,却忽视了用动态的视角以及消息/事件驱动的方式构建任务流程。
异步在编程看来是反人类的,但是从业务角度看却是再合理不过的了。通过当的工具及技术,使用异步并不是难以企及的,它可以使应用的资源利用更加的高效,让应用的响应性更上一个台阶。
http://www.ituring.com.cn/article/130823