异步编程浅析

一.非阻塞和异步

借用知乎用户严肃的回答
在此总结下,同步和异步是针对消息通信机制,同步代表一个client发出一个调用,不管是远程调用还是本地调用,在没有得到结果之前就不返回,一直等到调用返回,就得到返回值了。
而异步调用恰恰相反,调用在发出之后,就直接返回了,没有返回结果。但是结果怎么办呢,这个就是本篇文章讨论的内容,一般被调用者会通过一系列的信号或者回调来将结果告诉调用者。
而对于阻塞非阻塞是在调用方在等待消息的时候的状态,不管是同步还是异步,调用请求发出后,如果调用发一直守候在那,占用着资源,那就是阻塞的,如果不管它了,先去做别的事情,那就是非阻塞的。
举一个网络上的例子:
老张爱喝茶,废话不说,煮开水。出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
1 老张把水壶放到火上,立等水开。(同步阻塞)
老张觉得自己有点傻
2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。
3 老张把响水壶放到火上,立等水开。(异步阻塞)
老张觉得这样傻等意义不大
4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
老张觉得自己聪明了。
所谓同步异步,只是对于水壶而言。普通水壶,同步;响水壶,异步。虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。所谓阻塞非阻塞,仅仅对于老张而言。立等的老张,阻塞;看电视的老张,非阻塞。情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。

二.观察者(Observer )模式

1.观察者模式介绍

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
说得明白些就是有一个被观察者,还有一些(可能不止一个)观察者,观察者通过注册来绑定被观察者,当被观察者有变化时,就会通知观察者。这么一种模式,就叫做观察者模式。
如下图:

2.观察者模式实现

在被观察者内部维护一个观察者的数组,当被观察者改变时,执行这个数组下的所有观察者的notufy方法即可。java实现例子(简单,无并发)如下:

//抽象观察者角色
public interface Watcher
{
    public void update(String str);

}
//抽象主题角色,watched:被观察
public interface Watched
{
    public void addWatcher(Watcher watcher);

    public void removeWatcher(Watcher watcher);

    public void notifyWatchers(String str);

}

定义具体的观察者和被观察者

public class ConcreteWatcher implements Watcher
{

    @Override
    public void update(String str)
    {
        System.out.println(str);
    }

}
import java.util.ArrayList;
import java.util.List;

public class ConcreteWatched implements Watched
{
    // 存放观察者
    private List<Watcher> list = new ArrayList<Watcher>();

    @Override
    public void addWatcher(Watcher watcher)
    {
        list.add(watcher);
    }

    @Override
    public void removeWatcher(Watcher watcher)
    {
        list.remove(watcher);
    }

    @Override
    public void notifyWatchers(String str)
    {
        // 自动调用实际上是主题进行调用的
        for (Watcher watcher: list)
        {
            watcher.update(str);
        }
    }

}

上面代码在被观察者ConcreteWatched内部维护了一个观察者的list,当被观察者发生改变时,调用 notifyWatchers来调用所有的观察者的方法。
当然,各个语言有自己已经实现好的观察者模式代码,不需要自己再额外编写,并且语言内部实现的模式考虑到了资源利用,并发处理,回收,异常处理等等其他情况,因此推荐使用系统自身的观察者模式实现。

三.发布/订阅(Publish/Subscribe)模式

发布/订阅模式和观察者模式很类似,都有一个数据产生方,都有一些数据接收方,它们还是有一些不一样的地方。

如上图,发布/订阅模式有一个调度中心,发布者将消息发布到调度中心。订阅者从调度中心订阅,并且从调度中心获得数据。这是一种不同的模式,观察者模式强调对象的行为,发布/订阅强调架构和组件。
在大多数的情况下,可以将观察者模式解耦成发布/订阅模式,因此往往很多时候这两种模式当做一种模式,其实问题不大。

四.响应式编程(Reactive programming)

响应式编程(下面简称Rx)在如今的web框架中占的比例越来越多。响应式编程的目标是提供一致的编程接口, 帮助开发者更方便的处理异步数据流,使软件开发更高效、更简洁。Rx是一个多语言的实现,已经支持多种语言包括Java、Swift、C++、.NET、JavaScript、Ruby、Groovy、Scala等等,支持的库包括: RxJava 、 RxSwift 、Rx.NET、RxJS、RXRuby等等。
参考资料:
英文
中文
其实本质上,Rx就是对观察者模式进行封装,一方面使得其拥有基本的异步消息传递能力而不需要处理线程同步以及并发等问题,另外还具备了很多其他功能。
比如android里面的代码(需要导入RxJava和RxAndroid):

  Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> subscriber) {
                Log.d(TAG, "call: threadId:" + Thread.currentThread().getId());
                subscriber.onStart();
                subscriber.onNext("Hello World!");
                subscriber.onCompleted();
            }
        })
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<String>() {
                    @Override
                    public void onCompleted() {
                        Log.d(TAG, "onCompleted: threadId:" + Thread.currentThread().getId());
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.e(TAG, "onError: threadId:" + Thread.currentThread().getId());
                    }

                    @Override
                    public void onNext(String s) {
                        Log.d(TAG, "onNext: threadId:" + Thread.currentThread().getId());
                        Log.i(TAG, "onNext: s = " + s);
                    }
                });

可以指定订阅者和被订阅者的线程,是io线程还是mainThrad线程,另外还有onCompletedonErroronNext三个回调方法,单凭这些,就基本能够满足异步的使用要求。
其他的一些Rx框架:
RxJava
RxAndroid
RxSwift
RxJs 4 and RxJs 5
Rx.Net
RxPy

五.js中异步编程的方法

1.回调函数(callback)

(1)介绍

函数A作为参数(函数引用)传递到另一个函数B中,并且这个函数B执行函数A。我们就说函数A叫做回调函数。如果没有名称(函数表达式),就叫做匿名回调函数。
实际上,也就是把函数作为参数传递。

(2)示例

首先使用一个事例来演示js中的callback:

var i = 0;
function sleep(ms, callback) {
    setTimeout(function () {
        console.log('我执行完啦!');
        i++;
        if (i >= 2) callback(new Error('i大于2'), null);
        else callback(null, i);
    }, ms);
}

sleep(3000, function (err,val) {
    if(err) console.log('出错啦:'+err.message);
    else console.log(val);
})

上面,将callback函数通过高阶函数,参数的方式传入进去,然后再在里面直接调用,外面就能够获取到数据了。

(3)原理

将函数作为参数传入另一个函数,其实这个参数是一个指针入口,等另一个函数执行完毕,会接下去执行这个指针入口处的函数,这样数据就能连贯起来。
注意:回调的本质还是发布订阅模式,将函数通过入参接入,相当于订阅了这个函数。
在node中,回调会在系统的事件循环中创建一个事件,系统会在每一个Tick访问这个事件循环,查看是否有回调产生,有的话执行这个回调函数。而回调产生是系统层去产生的,在windows下是IOCP,linux是libuv实现的线程池产生。

(4)小结

回调是使用最广泛的异步编程方式,但是其有几个最大的缺点:
a.多层嵌套时,可读性差,如下

step1(function (value1) {
step2(value1, function(value2) {
    step3(value2, function(value3) {
        step4(value3, function(value4) {
            // Do something with value4
        });
    });
});
});

b.异常处理无法在外部捕捉

try{
 setTimeout(function(){
    JSON.parse("{'a':'1'}")
    console.log("aaaa")
 },0)
}
catch(ex){
 console.log(ex); //不能catch到这个异常
}

c.流程不好控制
callback嵌套时的流程繁琐,对于有依赖的项目不能够独立分出来,造成了性能浪费。
比如,当C操作依赖于B操作和C操作,而B与A没有依赖关系时,不用第三方库(如async,eventproxy)的话,B与A本可以并行,却串行了,性能有很大的提升空间。

2.事件(events)(nodejs)

(1)介绍

nodejs内部含有events消息模块,主要用来进行消息的传递和接收

(2)示例

var events = require('events');//引入模块
var  x =new events.EventEmitter();//创建实例
x.on('y', function(a,b,c){
 console.log('it\'s work1!'+a+b+c);
});//订阅一个字段,可以是多个
x.emit('y','111','222', '3333');//发布一个字段
//注意:需要先订阅再发布

(3)原理

events事件其实就是在本地维护一个key-value的数组,然后事件触发时获取数组中的订阅者,然后运行订阅者的方法。就是一个js版的发布订阅模式。

(4)小结

events事件模型是发布订阅模型在nodejs中的显示应用,当然jquery中也有相应的插件。引入events可以解决回调函数中嵌套过多的情况,还能解决异常不能被捕获的情况。
虽然解决了嵌套过多的情况,但是每一次都需要发布和订阅,会使内存使用增多,以及代码处处是订阅的情况。

3.Promise

(1)介绍

Promises对象是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口。
Promise介绍
简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。比如,f1的回调函数f2,可以写成:

f1().then(f2);

(2)示例

var i = 0;
//函数返回promise
function sleep(ms) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log('我执行好了');
            i++;
            if (i >= 2) reject(new Error('i>=2'));
            else resolve(i);
        }, ms);
    })
}

sleep(1000).then(function (val) {
    console.log(val);
    return sleep(1000)
}).then(function (val) {
    console.log(val);
    return sleep(1000)
}).then(function (val) {
    console.log(val);
    return sleep(1000)
}).catch(function (err) {
    console.log('出错啦:' + err.message);
})

(3)原理

Promise本身是一个通过回调生成的状态机模型,用两个数组分别存成功队列和失败队列,然后then就是向队列中添加回调函数,resolve和reject就是更改的状态,状态改变并且触发回调函数。

(4)小结

异步执行的函数返回一个Promise对象,表明我只是给出一个承诺,不能立刻给你消息。等执行完毕之后就跳回调用的地方,执行then里面的函数,并且将参数作为入参返回。
Promise的好处是无论什么时候都能够返回,回调函数会立马执行。另一方面采用链式处理,避免了回调的函数嵌套。能够catch所有的错误,因此不必担心捕捉不到错误。与事件events相比,Promise里面的状态(resolved,rejected)只要发生就固定了,不会改变,而事件events中必须及时去监听,如果错过了,那就监听不到了。
Promise目前也有一些缺点,不能取消,
Promise从ES6提出,主流的浏览器和js环境基本都支持了Promise的特性,目前使用越来越广泛。

4.async/await

(1)介绍

这中间其实还有一个异步方案Generator,但是自从async/await出来之后,跟Promise结合紧密,因此完全可以使用Promise+async/await来进行js的终极异步方案了。
不得不说,javascript在这一步落后了C#一大截,不过不算晚。async/await已经正式在ES7亮相。
async/await在node7.0中出现,需要使用harmony模式运行,在7.6以上就能够直接使用了。

(2)示例

定义一个异步函数:

async function fn(){
return 0;
}

其实返回的就是一个Promise。
await写在async中,此处promise其实就是C#中的Task,async和await和C#中的async/await使用一样。
所以,只要是Task就能够await,而不一定是async返回的函数才能await。比如,一个http request返回的是一个promise,就能够进行await进行同步,或者一个settimeout的函数,返回的是promise,也能使用await进行同步,如下:

const request = require('request');

const options = {
  url: '******',
  headers: {
    'User-Agent': 'request'
  }
};

const getRepoData = () => {  //一个http request
  return new Promise((resolve, reject) => {
    request(options, (err, res, body) => {
      if (err) {
        reject(err);
      }
      resolve(body);
    });
  });
};

async function asyncFun() {//一个有http request的异步方法
 try {
    const value = await getRepoData();
    // ... 和上面的yield类似,如果有多个异步流程,可以放在这里,比如
    // const r1 = await getR1();
    // const r2 = await getR2();
    // const r3 = await getR3();
    // 每个await相当于暂停,执行await之后会等待它后面的函数(不是generator)返回值之后再执行后面其它的await逻辑。
    return value;
  } catch (err) {
    console.log(err);
  }
}

asyncFun().then(x => console.log(`x: ${x}`)).catch(err => console.error(err));

(3)原理

async/await是一个语法糖,内部原理还是和Promise一样,使用回调和状态机进行控制,然后使用await进行阻塞控制同步,达到控制流程的目的,只是。。这使用方法和习惯也太像C#了。。

(4)小结

使用Promise处理异步函数,使用async/await处理异步函数的同步和步骤控制问题。async/await很好用,

5.RxJs

(1)介绍

RxJs是Rx家族的js版本,目前由ReactiveX组织维护,github仓库点此,它的第五版正在开发,是第四版的重构版本。
RxJs是Promise的高级版本,包含了Promise中一些没有的特性,比如cancel属性。

(2)示例

例子采用Rxjs 4

/* Get stock data somehow */
const source = getAsyncStockData();

const subscription = source
  .filter(quote => quote.price > 30)
  .map(quote => quote.price)
  .subscribe(
    price => console.log(`Prices higher than $30: ${price}`),
    err => console.log(`Something went wrong: ${err.message}`)
  );

/* When we're done */
subscription.dispose();

使用也很简单,订阅一个异步数据流source,然后采用builder的形式对数据进行处理,简洁明了。最后释放资源。和其他Rx的使用类似。

(3)原理

RxJs是js中的观察者模式,是比events更加高级的一种封装。

(4)小结

RxJs在总体上是Promise的升级版,添加了cancel,可以emit多个值。一般的中小型项目中采用Promise+async/await已经足够,除非是一些大型项目,需要进行一些复杂的操作,比如取消操作,多值传递等等。

6.总结

以上5种+Generator都是js中的异步处理方案,在条件允许下,尽量使用Promise+async/await进行异步处理和流程控制,个人认为的优先级 async/await > Promise/Generator > events > callback

六.其他语言中的异步编程方法

其实语言之间相差不大,有些思想值得相互借鉴

1.python

(1)协程(coroutine)
(2)yield,生成器(Generator)
(3)yield from (从python3.3开始)
(4)asyncio模块(从python3.4开始)
(5)async/await(从python3.5开始)//推荐
(6)附加:RxPy,看来有了async/await就没必要了
于是乎~python从3.5版本开始也提供了async/await来支持原生协程

2.java

(1)Future 和 FutureTask,类似于js中的Promise,或者C#中的Task
(2)第三方框架Netty,就是实现了一整套从Future,到callback的异步框架
(3)RxJava
(4)第三方的事件,消息等等

3.C#中的异步编程

(1)async/await //推荐
无疑C#中的异步编程思想是超前的,在C#5.0版本就推出了async/await异步流程控制,配合Task的异步方法任务,达到了异步编程的目的。
(2)Rx.Net

七.异步编程总结

1.未来的异步编程主要分为两大类

(1)以async/await +FutureTask/Promise/Task为主的思想,由语言提供原生的代码支持

(2)由Rx提供第三方的消息形式的发布订阅模式。 目前在国内还不温不火。

希望越来越多的语言能够体验到async/await语法的便利之处。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容