一.非阻塞和異步
借用知乎用戶嚴(yán)肅的回答
在此總結(jié)下,同步和異步是針對(duì)消息通信機(jī)制,同步代表一個(gè)client發(fā)出一個(gè)調(diào)用,不管是遠(yuǎn)程調(diào)用還是本地調(diào)用危彩,在沒(méi)有得到結(jié)果之前就不返回,一直等到調(diào)用返回泳桦,就得到返回值了汤徽。
而異步調(diào)用恰恰相反,調(diào)用在發(fā)出之后灸撰,就直接返回了谒府,沒(méi)有返回結(jié)果。但是結(jié)果怎么辦呢浮毯,這個(gè)就是本篇文章討論的內(nèi)容狱掂,一般被調(diào)用者會(huì)通過(guò)一系列的信號(hào)或者回調(diào)來(lái)將結(jié)果告訴調(diào)用者。
而對(duì)于阻塞非阻塞是在調(diào)用方在等待消息的時(shí)候的狀態(tài)亲轨,不管是同步還是異步趋惨,調(diào)用請(qǐng)求發(fā)出后,如果調(diào)用發(fā)一直守候在那惦蚊,占用著資源器虾,那就是阻塞的,如果不管它了蹦锋,先去做別的事情兆沙,那就是非阻塞的。
舉一個(gè)網(wǎng)絡(luò)上的例子:
老張愛(ài)喝茶莉掂,廢話不說(shuō)葛圃,煮開(kāi)水。出場(chǎng)人物:老張,水壺兩把(普通水壺库正,簡(jiǎn)稱水壺曲楚;會(huì)響的水壺,簡(jiǎn)稱響水壺)褥符。
1 老張把水壺放到火上龙誊,立等水開(kāi)。(同步阻塞)
老張覺(jué)得自己有點(diǎn)傻
2 老張把水壺放到火上喷楣,去客廳看電視趟大,時(shí)不時(shí)去廚房看看水開(kāi)沒(méi)有。(同步非阻塞)
老張還是覺(jué)得自己有點(diǎn)傻铣焊,于是變高端了逊朽,買了把會(huì)響笛的那種水壺。水開(kāi)之后曲伊,能大聲發(fā)出嘀~~~~的噪音叽讳。
3 老張把響水壺放到火上,立等水開(kāi)熊昌。(異步阻塞)
老張覺(jué)得這樣傻等意義不大
4 老張把響水壺放到火上绽榛,去客廳看電視湿酸,水壺響之前不再去看它了婿屹,響了再去拿壺。(異步非阻塞)
老張覺(jué)得自己聰明了推溃。
所謂同步異步昂利,只是對(duì)于水壺而言。普通水壺铁坎,同步蜂奸;響水壺,異步硬萍。雖然都能干活扩所,但響水壺可以在自己完工之后,提示老張水開(kāi)了朴乖。這是普通水壺所不能及的祖屏。同步只能讓調(diào)用者去輪詢自己(情況2中),造成老張效率的低下买羞。所謂阻塞非阻塞袁勺,僅僅對(duì)于老張而言。立等的老張畜普,阻塞期丰;看電視的老張,非阻塞。情況1和情況3中老張就是阻塞的钝荡,媳婦喊他都不知道街立。雖然3中響水壺是異步的,可對(duì)于立等的老張沒(méi)有太大的意義化撕。所以一般異步是配合非阻塞使用的几晤,這樣才能發(fā)揮異步的效用。
二.觀察者(Observer )模式
1.觀察者模式介紹
觀察者模式定義了一種一對(duì)多的依賴關(guān)系植阴,讓多個(gè)觀察者對(duì)象同時(shí)監(jiān)聽(tīng)某一個(gè)主題對(duì)象蟹瘾。這個(gè)主題對(duì)象在狀態(tài)發(fā)生變化時(shí),會(huì)通知所有觀察者對(duì)象掠手,使它們能夠自動(dòng)更新自己憾朴。
說(shuō)得明白些就是有一個(gè)被觀察者,還有一些(可能不止一個(gè))觀察者喷鸽,觀察者通過(guò)注冊(cè)來(lái)綁定被觀察者众雷,當(dāng)被觀察者有變化時(shí),就會(huì)通知觀察者做祝。這么一種模式砾省,就叫做觀察者模式。
如下圖:
2.觀察者模式實(shí)現(xiàn)
在被觀察者內(nèi)部維護(hù)一個(gè)觀察者的數(shù)組混槐,當(dāng)被觀察者改變時(shí)编兄,執(zhí)行這個(gè)數(shù)組下的所有觀察者的notufy方法即可。java實(shí)現(xiàn)例子(簡(jiǎn)單声登,無(wú)并發(fā))如下:
//抽象觀察者角色
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)
{
// 自動(dòng)調(diào)用實(shí)際上是主題進(jìn)行調(diào)用的
for (Watcher watcher: list)
{
watcher.update(str);
}
}
}
上面代碼在被觀察者ConcreteWatched
內(nèi)部維護(hù)了一個(gè)觀察者的list,當(dāng)被觀察者發(fā)生改變時(shí),調(diào)用 notifyWatchers
來(lái)調(diào)用所有的觀察者的方法悯嗓。
當(dāng)然件舵,各個(gè)語(yǔ)言有自己已經(jīng)實(shí)現(xiàn)好的觀察者模式代碼,不需要自己再額外編寫(xiě)脯厨,并且語(yǔ)言內(nèi)部實(shí)現(xiàn)的模式考慮到了資源利用铅祸,并發(fā)處理,回收合武,異常處理等等其他情況临梗,因此推薦使用系統(tǒng)自身的觀察者模式實(shí)現(xiàn)。
三.發(fā)布/訂閱(Publish/Subscribe)模式
發(fā)布/訂閱模式和觀察者模式很類似眯杏,都有一個(gè)數(shù)據(jù)產(chǎn)生方夜焦,都有一些數(shù)據(jù)接收方,它們還是有一些不一樣的地方岂贩。
如上圖茫经,發(fā)布/訂閱模式有一個(gè)調(diào)度中心巷波,發(fā)布者將消息發(fā)布到調(diào)度中心。訂閱者從調(diào)度中心訂閱卸伞,并且從調(diào)度中心獲得數(shù)據(jù)抹镊。這是一種不同的模式,觀察者模式強(qiáng)調(diào)對(duì)象的行為荤傲,發(fā)布/訂閱強(qiáng)調(diào)架構(gòu)和組件垮耳。
在大多數(shù)的情況下,可以將觀察者模式解耦成發(fā)布/訂閱模式遂黍,因此往往很多時(shí)候這兩種模式當(dāng)做一種模式终佛,其實(shí)問(wèn)題不大。
四.響應(yīng)式編程(Reactive programming)
響應(yīng)式編程(下面簡(jiǎn)稱Rx)在如今的web框架中占的比例越來(lái)越多雾家。響應(yīng)式編程的目標(biāo)是提供一致的編程接口铃彰, 幫助開(kāi)發(fā)者更方便的處理異步數(shù)據(jù)流,使軟件開(kāi)發(fā)更高效芯咧、更簡(jiǎn)潔牙捉。Rx是一個(gè)多語(yǔ)言的實(shí)現(xiàn),已經(jīng)支持多種語(yǔ)言包括Java敬飒、Swift邪铲、C++、.NET无拗、JavaScript带到、Ruby、Groovy蓝纲、Scala等等阴孟,支持的庫(kù)包括: RxJava 晌纫、 RxSwift 税迷、Rx.NET、RxJS锹漱、RXRuby等等箭养。
參考資料:
英文
中文
其實(shí)本質(zhì)上,Rx就是對(duì)觀察者模式進(jìn)行封裝哥牍,一方面使得其擁有基本的異步消息傳遞能力而不需要處理線程同步以及并發(fā)等問(wèn)題毕泌,另外還具備了很多其他功能。
比如android里面的代碼(需要導(dǎo)入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線程撼泛,另外還有onCompleted
,onError
澡谭,onNext
三個(gè)回調(diào)方法愿题,單憑這些,就基本能夠滿足異步的使用要求。
其他的一些Rx框架:
RxJava
RxAndroid
RxSwift
RxJs 4 and RxJs 5
Rx.Net
RxPy
五.js中異步編程的方法
1.回調(diào)函數(shù)(callback)
(1)介紹
函數(shù)A作為參數(shù)(函數(shù)引用)傳遞到另一個(gè)函數(shù)B中潘酗,并且這個(gè)函數(shù)B執(zhí)行函數(shù)A杆兵。我們就說(shuō)函數(shù)A叫做回調(diào)函數(shù)。如果沒(méi)有名稱(函數(shù)表達(dá)式)仔夺,就叫做匿名回調(diào)函數(shù)琐脏。
實(shí)際上,也就是把函數(shù)作為參數(shù)傳遞缸兔。
(2)示例
首先使用一個(gè)事例來(lái)演示js中的callback:
var i = 0;
function sleep(ms, callback) {
setTimeout(function () {
console.log('我執(zhí)行完啦日裙!');
i++;
if (i >= 2) callback(new Error('i大于2'), null);
else callback(null, i);
}, ms);
}
sleep(3000, function (err,val) {
if(err) console.log('出錯(cuò)啦:'+err.message);
else console.log(val);
})
上面,將callback函數(shù)通過(guò)高階函數(shù)惰蜜,參數(shù)的方式傳入進(jìn)去阅签,然后再在里面直接調(diào)用,外面就能夠獲取到數(shù)據(jù)了蝎抽。
(3)原理
將函數(shù)作為參數(shù)傳入另一個(gè)函數(shù)政钟,其實(shí)這個(gè)參數(shù)是一個(gè)指針入口,等另一個(gè)函數(shù)執(zhí)行完畢樟结,會(huì)接下去執(zhí)行這個(gè)指針入口處的函數(shù)养交,這樣數(shù)據(jù)就能連貫起來(lái)。
注意:回調(diào)的本質(zhì)還是發(fā)布訂閱模式瓢宦,將函數(shù)通過(guò)入?yún)⒔尤胨榱喈?dāng)于訂閱了這個(gè)函數(shù)。
在node中驮履,回調(diào)會(huì)在系統(tǒng)的事件循環(huán)中創(chuàng)建一個(gè)事件鱼辙,系統(tǒng)會(huì)在每一個(gè)Tick訪問(wèn)這個(gè)事件循環(huán),查看是否有回調(diào)產(chǎn)生玫镐,有的話執(zhí)行這個(gè)回調(diào)函數(shù)倒戏。而回調(diào)產(chǎn)生是系統(tǒng)層去產(chǎn)生的,在windows下是IOCP恐似,linux是libuv實(shí)現(xiàn)的線程池產(chǎn)生杜跷。
(4)小結(jié)
回調(diào)是使用最廣泛的異步編程方式,但是其有幾個(gè)最大的缺點(diǎn):
a.多層嵌套時(shí)矫夷,可讀性差葛闷,如下
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});
b.異常處理無(wú)法在外部捕捉
try{
setTimeout(function(){
JSON.parse("{'a':'1'}")
console.log("aaaa")
},0)
}
catch(ex){
console.log(ex); //不能catch到這個(gè)異常
}
c.流程不好控制
callback嵌套時(shí)的流程繁瑣,對(duì)于有依賴的項(xiàng)目不能夠獨(dú)立分出來(lái)双藕,造成了性能浪費(fèi)淑趾。
比如,當(dāng)C操作依賴于B操作和C操作,而B(niǎo)與A沒(méi)有依賴關(guān)系時(shí)忧陪,不用第三方庫(kù)(如async,eventproxy)的話,B與A本可以并行扣泊,卻串行了驳概,性能有很大的提升空間。
2.事件(events)(nodejs)
(1)介紹
nodejs內(nèi)部含有events消息模塊旷赖,主要用來(lái)進(jìn)行消息的傳遞和接收
(2)示例
var events = require('events');//引入模塊
var x =new events.EventEmitter();//創(chuàng)建實(shí)例
x.on('y', function(a,b,c){
console.log('it\'s work1!'+a+b+c);
});//訂閱一個(gè)字段顺又,可以是多個(gè)
x.emit('y','111','222', '3333');//發(fā)布一個(gè)字段
//注意:需要先訂閱再發(fā)布
(3)原理
events事件其實(shí)就是在本地維護(hù)一個(gè)key-value的數(shù)組,然后事件觸發(fā)時(shí)獲取數(shù)組中的訂閱者等孵,然后運(yùn)行訂閱者的方法稚照。就是一個(gè)js版的發(fā)布訂閱模式。
(4)小結(jié)
events事件模型是發(fā)布訂閱模型在nodejs中的顯示應(yīng)用俯萌,當(dāng)然jquery中也有相應(yīng)的插件果录。引入events可以解決回調(diào)函數(shù)中嵌套過(guò)多的情況,還能解決異常不能被捕獲的情況咐熙。
雖然解決了嵌套過(guò)多的情況弱恒,但是每一次都需要發(fā)布和訂閱,會(huì)使內(nèi)存使用增多棋恼,以及代碼處處是訂閱的情況返弹。
3.Promise
(1)介紹
Promises對(duì)象是CommonJS工作組提出的一種規(guī)范,目的是為異步編程提供統(tǒng)一接口爪飘。
Promise介紹
簡(jiǎn)單說(shuō)义起,它的思想是,每一個(gè)異步任務(wù)返回一個(gè)Promise對(duì)象师崎,該對(duì)象有一個(gè)then方法默终,允許指定回調(diào)函數(shù)。比如犁罩,f1的回調(diào)函數(shù)f2,可以寫(xiě)成:
f1().then(f2);
(2)示例
var i = 0;
//函數(shù)返回promise
function sleep(ms) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('我執(zhí)行好了');
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('出錯(cuò)啦:' + err.message);
})
(3)原理
Promise本身是一個(gè)通過(guò)回調(diào)生成的狀態(tài)機(jī)模型齐蔽,用兩個(gè)數(shù)組分別存成功隊(duì)列和失敗隊(duì)列,然后then就是向隊(duì)列中添加回調(diào)函數(shù)床估,resolve和reject就是更改的狀態(tài)含滴,狀態(tài)改變并且觸發(fā)回調(diào)函數(shù)。
(4)小結(jié)
異步執(zhí)行的函數(shù)返回一個(gè)Promise對(duì)象顷窒,表明我只是給出一個(gè)承諾蛙吏,不能立刻給你消息源哩。等執(zhí)行完畢之后就跳回調(diào)用的地方鞋吉,執(zhí)行then里面的函數(shù),并且將參數(shù)作為入?yún)⒎祷亍?br>
Promise的好處是無(wú)論什么時(shí)候都能夠返回励烦,回調(diào)函數(shù)會(huì)立馬執(zhí)行谓着。另一方面采用鏈?zhǔn)教幚恚苊饬嘶卣{(diào)的函數(shù)嵌套坛掠。能夠catch所有的錯(cuò)誤赊锚,因此不必?fù)?dān)心捕捉不到錯(cuò)誤治筒。與事件events相比,Promise里面的狀態(tài)(resolved舷蒲,rejected)只要發(fā)生就固定了耸袜,不會(huì)改變,而事件events中必須及時(shí)去監(jiān)聽(tīng)牲平,如果錯(cuò)過(guò)了堤框,那就監(jiān)聽(tīng)不到了。
Promise目前也有一些缺點(diǎn)纵柿,不能取消蜈抓,
Promise從ES6提出,主流的瀏覽器和js環(huán)境基本都支持了Promise的特性昂儒,目前使用越來(lái)越廣泛沟使。
4.async/await
(1)介紹
這中間其實(shí)還有一個(gè)異步方案Generator,但是自從async/await出來(lái)之后渊跋,跟Promise結(jié)合緊密腊嗡,因此完全可以使用Promise+async/await來(lái)進(jìn)行js的終極異步方案了。
不得不說(shuō)拾酝,javascript在這一步落后了C#一大截叽唱,不過(guò)不算晚。async/await已經(jīng)正式在ES7亮相微宝。
async/await在node7.0中出現(xiàn)棺亭,需要使用harmony模式運(yùn)行,在7.6以上就能夠直接使用了蟋软。
(2)示例
定義一個(gè)異步函數(shù):
async function fn(){
return 0;
}
其實(shí)返回的就是一個(gè)Promise镶摘。
await寫(xiě)在async中,此處promise其實(shí)就是C#中的Task岳守,async和await和C#中的async/await使用一樣凄敢。
所以,只要是Task就能夠await湿痢,而不一定是async返回的函數(shù)才能await涝缝。比如,一個(gè)http request返回的是一個(gè)promise譬重,就能夠進(jìn)行await進(jìn)行同步拒逮,或者一個(gè)settimeout的函數(shù),返回的是promise臀规,也能使用await進(jìn)行同步滩援,如下:
const request = require('request');
const options = {
url: '******',
headers: {
'User-Agent': 'request'
}
};
const getRepoData = () => { //一個(gè)http request
return new Promise((resolve, reject) => {
request(options, (err, res, body) => {
if (err) {
reject(err);
}
resolve(body);
});
});
};
async function asyncFun() {//一個(gè)有http request的異步方法
try {
const value = await getRepoData();
// ... 和上面的yield類似,如果有多個(gè)異步流程塔嬉,可以放在這里玩徊,比如
// const r1 = await getR1();
// const r2 = await getR2();
// const r3 = await getR3();
// 每個(gè)await相當(dāng)于暫停租悄,執(zhí)行await之后會(huì)等待它后面的函數(shù)(不是generator)返回值之后再執(zhí)行后面其它的await邏輯。
return value;
} catch (err) {
console.log(err);
}
}
asyncFun().then(x => console.log(`x: ${x}`)).catch(err => console.error(err));
(3)原理
async/await是一個(gè)語(yǔ)法糖恩袱,內(nèi)部原理還是和Promise一樣泣棋,使用回調(diào)和狀態(tài)機(jī)進(jìn)行控制,然后使用await進(jìn)行阻塞控制同步畔塔,達(dá)到控制流程的目的外傅,只是。俩檬。這使用方法和習(xí)慣也太像C#了萎胰。。
(4)小結(jié)
使用Promise處理異步函數(shù)棚辽,使用async/await處理異步函數(shù)的同步和步驟控制問(wèn)題技竟。async/await很好用,
5.RxJs
(1)介紹
RxJs是Rx家族的js版本屈藐,目前由ReactiveX組織維護(hù)榔组,github倉(cāng)庫(kù)點(diǎn)此,它的第五版正在開(kāi)發(fā),是第四版的重構(gòu)版本联逻。
RxJs是Promise的高級(jí)版本搓扯,包含了Promise中一些沒(méi)有的特性,比如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();
使用也很簡(jiǎn)單锨推,訂閱一個(gè)異步數(shù)據(jù)流source,然后采用builder的形式對(duì)數(shù)據(jù)進(jìn)行處理公壤,簡(jiǎn)潔明了换可。最后釋放資源。和其他Rx的使用類似厦幅。
(3)原理
RxJs是js中的觀察者模式沾鳄,是比events更加高級(jí)的一種封裝。
(4)小結(jié)
RxJs在總體上是Promise的升級(jí)版确憨,添加了cancel译荞,可以emit多個(gè)值。一般的中小型項(xiàng)目中采用Promise+async/await已經(jīng)足夠休弃,除非是一些大型項(xiàng)目吞歼,需要進(jìn)行一些復(fù)雜的操作,比如取消操作玫芦,多值傳遞等等浆熔。
6.總結(jié)
以上5種+Generator都是js中的異步處理方案,在條件允許下桥帆,盡量使用Promise+async/await進(jìn)行異步處理和流程控制医增,個(gè)人認(rèn)為的優(yōu)先級(jí) async/await > Promise/Generator > events > callback
六.其他語(yǔ)言中的異步編程方法
其實(shí)語(yǔ)言之間相差不大,有些思想值得相互借鑒
1.python
(1)協(xié)程(coroutine)
(2)yield老虫,生成器(Generator)
(3)yield from (從python3.3開(kāi)始)
(4)asyncio模塊(從python3.4開(kāi)始)
(5)async/await(從python3.5開(kāi)始)//推薦
(6)附加:RxPy叶骨,看來(lái)有了async/await就沒(méi)必要了
于是乎~python從3.5版本開(kāi)始也提供了async/await來(lái)支持原生協(xié)程
2.java
(1)Future 和 FutureTask,類似于js中的Promise祈匙,或者C#中的Task
(2)第三方框架Netty忽刽,就是實(shí)現(xiàn)了一整套從Future,到callback的異步框架
(3)RxJava
(4)第三方的事件夺欲,消息等等
3.C#中的異步編程
(1)async/await //推薦
無(wú)疑C#中的異步編程思想是超前的跪帝,在C#5.0版本就推出了async/await異步流程控制,配合Task的異步方法任務(wù)些阅,達(dá)到了異步編程的目的伞剑。
(2)Rx.Net
七.異步編程總結(jié)
1.未來(lái)的異步編程主要分為兩大類
(1)以async/await +FutureTask/Promise/Task為主的思想,由語(yǔ)言提供原生的代碼支持
(2)由Rx提供第三方的消息形式的發(fā)布訂閱模式市埋。 目前在國(guó)內(nèi)還不溫不火黎泣。
希望越來(lái)越多的語(yǔ)言能夠體驗(yàn)到async/await語(yǔ)法的便利之處。