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


    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

  • 相关阅读:
    装饰者模式【结构模式】
    代理模式【结构模式】
    原型模式【构建模式】
    建造者模式【构建模式】
    抽象工厂模式【构建模式】
    工厂模式【构建模式】
    单例模式【构建模式】
    设计原则
    Collector 源码分析
    Maven 包命令
  • 原文地址:https://www.cnblogs.com/feng9exe/p/10278813.html
Copyright © 2020-2023  润新知