响应式编程-异步编程的问题与困难

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.jswaterfall来简化:

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

posted @ 2019-01-16 18:58  zzfx  阅读(1358)  评论(0编辑  收藏  举报