使用異步編程

轉發至:http://www.ituring.com.cn/article/130823javascript

導言

現代的應用程序面臨着諸多的挑戰,如何構建具備可伸縮性和高性能的應用成爲愈來愈多軟件開發者思考的問題。隨着應用規模的不斷增大,業務複雜性的增加以及實時處理需求的增長,開發者不斷嘗試榨取硬件資源、優化。html

在不斷的探索中,出現了不少簡化場景的工具,好比提供可伸縮計算資源的Amazon S3Windows Azure,針對大數據的數據挖掘工具MapReduce,各類CDN服務,雲存儲服務等等。還有不少的工程實踐例如敏捷DDD等提供了指導。能夠看到,將每一個關注層面以服務的方式提供,成爲了愈來愈流行的一種模式,或許咱們能夠激進的認爲,這就是SOAjava

開發者須要將不一樣的資源粘合在一塊兒來提供最終的應用,這就須要協調不一樣的資源。node

咱們能夠設想一個大的場景,開發者正在開發的一個用例會從用戶的瀏覽器接收到請求,該請求會先從一個開放主機服務(OHS)獲取必要的資源res1,而後調用本機的服務s1對資源res1進行適應的轉換產生資源res2,接着以res2爲參數調用遠程的數據倉庫服務rs1獲取業務數據bs1,最後以bs1爲參數調用本機的計算服務calc並通過10s產生最終的數據。jquery

簡單的用ASP.NET MVC 5表示就是這樣的(這些代碼是我瞎掰的):git

// notes: ASP.NET vNext changed MVC 5 usage, 
// ActionResult now became IActionResult
public IActionResult CrazyCase(UserData userData) {
    var ticket = CrazyApplication.Ticket;

    var ohsFactory = new OpenHostServiceFactory(ticket);
    var ohs = ohsFactory.CreateService();

    var ohsAdapter = new OhsAdapter(userData);

    var rs1 = ohs.RetrieveResource(ohsAdapter);
    var rs2 = _localConvertingService.Unitize(rs1);
    var bs1 = _remoteRepository.LoadBusinessData(rs2);
    var result = _calculationService.DoCalculation(bs1);

    return View(result);
}

這多是中等複雜度的一個場景,可是相信開發者已經意識到了這其中所涉及的複雜度。咱們看到每一步都是依賴於前者所產生的數據,在這樣一種場景之下,傳統的多線程技術將極度受限,而且最頂層的協調服務將始終佔用一個線程來協調每一步。github

線程是要增長開銷的,尤爲是上下文的轉換,別扯什麼線程池了,建立線程的開銷是節省了,上下文切換的開銷纔是要命的。web

經濟不景氣,能省點兒資源就省點兒吧。ajax


因此咱們該怎麼辦?縱向擴展給服務器加多點內存?橫向擴展上負載均衡?別鬧了咱們又不是民工,想問題不要太簡單粗暴。解決的辦法就是,異步,並且咱們這篇也只討論異步這一種技術。算法

爲何使用異步

那麼,異步的優點在哪裏?這首先要和同步作一個對比。

仍是開頭那個場景,示例代碼所展現的是使用同步阻塞的方式來一步一步的執行,以下示意:

main) +++$----$------$--------$----------$+++
         |   /|     /|       /|         /
ohs )    $++$ |    / |      / |        /
              |   /  |     /  |       /
rs1 )         $++$   |    /   |      /
                     |   /    |     /
s1  )                $++$     |    /
                              |   /
calc)                         $++$

notes:
$ code point
+ thread busy
- thread blocked(means, wasted)

能夠明顯的看到,當主線程發起各個service請求後,徹底處於閒置佔用的狀態,所作的無非是協調任務間的依賴順序。這裏所說的佔用,其實就是CPU的時間片。

咱們爲何要等全部的子任務結束?由於任務間有前後順序依賴。有沒有更好的方式來規避等待所帶來的損耗呢?考慮一個場景,正上着班呢,忽然想起要在網上買個東西,那麼打開京東你就順利的下單了,事情並無結束,你不會等快遞的小哥給你送來東西之後再接着今天的工做吧?你會給快遞留下你的聯繫方式,讓他到了給你打電話(耗時的I/O任務),而後你繼續今天燒腦的編程任務(CPU密集型)。從人類的角度來看,這必定是最正常不過的,也就是要討論的異步的方式。

必定有人會提議單開一個線程作收快遞的任務,我贊成這是一種解決方案,可是若是用等效的人類角度的語言來講,就是你將大腦的資源分紅了兩半,一半在燒腦編程,一半在盯着手機發呆,腦利用率降低太明顯。而用異步的方式,你不須要關注手機,由於手機響了你就天然獲得了通知。 固然,你也能夠任性的說,我就喜歡等快遞來了再幹活。if so,咱們就不要作朋友了。

因此咱們能夠有一個推論:異步所解決的,就是節省低速的IO所阻塞的CPU計算時間。

轉換一下思路,咱們使用異步非阻塞的方式來構建這段業務,並藉助異步思想早已深刻人心的javascript語言來解釋,能夠是這樣的:

// express

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;

    // level1 nest
    ohs.retrieveResource(userData, function(err, rs1) {
        if(err) {
            // error handling
        }
        // level2 nest
        localConvertingService.unitize(rs1, function(err, rs2) {
            if(err) {
                // error handling
            }
            //level3 nest
            remoteRepository.loadBusinessData(rs2, function(err, bs1) {
                if(err) {
                    // error handling
                }
                //level4 nest
                calculationService.doCalculation(bs1, function(err, result) {
                    if(err) {
                        // error handling
                    }
                    res.view(result);
                });
            });
        });
    });
}

看着一層又一層的花括號也是醉了,咱們以後會討論如何解嵌套。那麼這段代碼所反應的是怎樣的事實呢?以下示意:

main) +++$                           $+++
          \                         /
ohs )      $++$                    /
               \                  /
rs1 )           $++$             /
                    \           /
s1  )                $++$      /
                         \    /
calc)                     $++$

notes:
$ code point
+ thread busy
- thread blocked(means, wasted)

因爲異步解放了原始的工做線程,使CPU資源能夠不被線程的阻塞而被浪費,從而能夠有效的提升吞吐率。

異步的使用場景

技術和選擇和使用場景有着很大的關係,每項技術不都是銀彈,使用對的工具/技術解決對的問題是開發者的義務。

開發者最多關注的是計算密集和I/O密集這兩個維度,對於這兩個維度每每有着不一樣的技術選型。

計算密集型應用

何爲計算密集型應用?下面兩我的畜皆知的函數都是計算密集型的。

 1 // F#
 2 let fibonacci n =
 3     let rec f a b n =
 4         match n with
 5         | 0 -> a
 6         | 1 -> b
 7         | n -> (f b (a + b) (n - 1))
 8     f 0 1 n
 9 
10 let rec factorial n = 
11     match n with
12     | 0 -> 1
13     | n -> n * factorial (n - 1)

尤爲是第二個階乘函數,若是在調用的時候不當心手抖多加了幾個0,基本上能夠出去喝個咖啡談談理想聊聊人生玩一天再回來看看有沒有算完了。

簡而言之,計算密集型的任務是典型的重度依賴CPU/GPU,不涉及磁盤、網絡、輸入輸出的任務。遊戲中場景渲染是計算密集的,MapReduce中的Reduce部分是計算密集的,視頻處理軟件的實時渲染是計算密集的,等等。

在這樣的場景之下,異步是沒有太大的優點的,由於計算資源就那麼多,不增不減,用多線程也好用異步流也好,CPU永遠處於高負荷狀態,這病不能治,解決方案只能是:

  • 橫向的集羣方案
  • 縱向的升級主機CPU或採用更快的GPU
  • 優化算法,使之空間/時間成本下降

可是有一種場景是能夠考慮使用異步的,考慮一個分佈式的計算場,一個計算任務發起後,協調者須要等待全部的計算節點子結果集返回後者能作最後的結果化簡。那麼此時,雖然場景是計算密集的,可是因爲涉及到任務的依賴協調,採用異步的方式,能夠避免等待節點返回結果時的阻塞,也能夠避免多線程方式的上下文切換開銷,要知道在這樣的場景下,上下文切換的開銷是能夠大的驚人的。

類似的場景還有,一個桌面應用,假設點擊界面上一個按鈕以後會進行大量的計算,若是採用同步阻塞的方式,那麼當計算完成以前UI是徹底阻塞的跟假死同樣,可是如何使用異步的方式,則不會發生UI阻塞,計算在結束後會以異步的方式來更新界面。還記得WinForm編程中的BeginInvokeEndInvoke嗎?雖然它們的實現方式是以單獨線程的方式來實現異步操做的,可是這仍然屬於異步流控制的範疇。

異步的實現方式有不少,可使用已有的線程技術(Rx和C#的async/await就是使用這種方式),也可使用相似於libuv之類的I/O異步封裝配合事件驅動(node就是使用這種方式)。並於異步流控制的部分咱們以後會討論。

因此若是你的應用是計算密集型的,在充分分析場景的前提下能夠適當的採用異步的方式。大部分的計算密集型場景是不用介入異步控制技術的,除非它能夠顯著改善應用的流程控制能力。

I/O密集型應用

何爲I/O密集型應用?Web服務器自然就是I/O密集型的,由於有着高併發量與網絡吞吐。文件服務器和CDN是I/O密集型的,由於高網絡吞吐高磁盤訪問量。數據庫是I/O密集型的,涉及磁盤的訪問及網絡訪問。說到底,一切和輸入輸出相關的場景都是I/O密集型的。

I/O囊括的方面主要是兩方面:

  • 網絡訪問
  • 磁盤讀寫

簡單粗暴的解釋,就是接在主板南橋上的設備的訪問都屬於I/O。多提一句,內存是直接接在北橋上的,這貨,快。

開發者遇到最多的場景即是Web應用和數據庫的高併發訪問。其它的服務調用都屬於網絡I/O,可歸爲一類。

典型的就是Web服務器接收到了HTTP請求,而後具體的Web框架會單開一個線程服務這個請求。由於HTTP是構建在TCP之上的,因此在請求結束返回結果以前,socket並無關閉,在windows系統上這就是一個句柄,在*nix之類的posix系統上這就是一個文件描述符,都是系統資源緊張的很。這是硬性的限制,能打開多少取決與內存與操做系統,咱們暫且不關注這部分。該線程若是採用同步的方式,那麼它程的生命週期會吻合socket的生命週期,期間無論是訪問文件系統花了10s致使cpu空閒10s的時間片,仍是訪問數據庫有3s的時間片空隙,這個線程都不會釋放,就是說,這個線程是專屬的,即使是使用線程池技術,該佔還得佔。

這有點像是銀行的VIP專線,服務人員就那麼多,若是每人服務一個VIP且甭管人家在聊人生聊理想仍是默默注視,後面人就算是VIP也得等着,由於沒人能夠服務你了。

那麼咱們繼續深刻,線程也是一種相對昂貴的資源,雖然比建立進程快了太多,可是仍然有限制。windows的32位操做系統默認每進程可以使用2GB用戶態內存(64bit是8Tb用戶態內存, LoL),每一個線程有1Mb的棧空間(能改,但不建議。);*nix下是8Mb棧空間,32位的進程空間是4Gb,64位則大到幾近沒有用戶態內存限制。咱們能夠假定32位系統下一個合理的單進程線程數量:1500。那麼一個進程最大的併發量就是1500請求了,拋開多核不談,這1500個線程就算輪班倒,併發量不會再上去了,由於一個socket一個線程。若是每一個請求都是web服務器處理1s加訪問數據庫服務器3s,那麼時鐘浪費率則大的驚人。何況,1500個線程的上下文切換想一想都是開心,開了又開

不幸的是,以前的web服務器都是這麼幹的。此時咱們思考,若是採用異步的方式,那3s的阻塞徹底能夠規避,從而使線程輪轉的更快,由於1s的處理時間結束後線程返回線程池而後服務於另外一個請求,從而總體提升服務器的吞率。

事實上,node壓根就沒有多線程的概念,使用事件循環配合異步I/O,一個線程總夠你甩傳統的Web服務器吞吐量幾條街。沒錯,請叫我node雷鋒。

再繼續深刻異步編程前,咱們先理一理幾個常常混淆的概念。

一些概念的區別

多核與多線程

多核是一種物理上的概念,即指主機所擁有的物理CPU核心數量,總核心數 = CPU個數 * 每一個CPU的核心數。每一個核心是獨立的,能夠同時服務於不一樣的進程/線程。

多線程是一種操做系統上的概念,單個進程可能建立多個線程來達到細粒度進行流程控制的目的。操做系統的核心態調度進程與線程,在用戶態之下其實還能夠對單個線程有更細粒度的控制,這稱之爲協程(coroutine)纖程(fibers)

多線程是指在單個進程空間內經過操做系統的調度來達到多流程同時執行的一種機制,固然,單個CPU核心在單位時間內永遠都只是執行一個線程的指令,因此須要以小的時間片斷雨露均沾的執行每一個線程的部分指令。在切換線程時是有上下文的切換的,包括寄存器的保存/還原,線程堆棧的保存/還原,這就是開銷。

並行與併發

關於並行,真相只有一個,單個CPU核心在單位時間內只能執行一個線程的指令,因此若是總核心數爲20,那麼咱們能夠認爲該主機的並行能力爲20,可是用戶態的並行能力是要比這個低的,由於操做系統服務和其它軟件也是要用cpu的,所以這個數值是達不到的。

一個題外話,若是並行能力爲20,那麼咱們能夠粗略的認爲,該主機一次能夠同時執行20個線程,若是程序的線程使用率健康的話,保持線程池爲20左右的大小能夠作到徹底的線程並行執行沒有上下文切換。

那麼併發則關注於應用的處理能力。這是一個更加側重網絡請求/服務響應能力的概念,能夠理解爲單位時間內能夠同時接納並處理用戶請求的能力。它和多少CPU沒有必然的關係,單純的考量了服務器的響應回覆能力。

阻塞與非阻塞

阻塞/非阻塞與同步/異步是常常被混淆的。同步/異步其實在說事件的執行順序,阻塞/非阻塞是指作一件事能不能當即返回。

咱們舉個去KFC點餐的例子。點完餐交完錢了,會有這麼幾種狀況:

  • 服務人員直接把東西給我,由於以前已經作好了,因此能立刻給我,這叫作非阻塞,我不須要等,結果當即返回。這整個過程是同步完成的。
  • 服務人員一看沒有現成的東西了,跑去現作,那麼我就在這兒一直等,沒刷微信沒作別的乾等,等到作出來拿走,這叫阻塞,由於我傻到等結果返回再離開點餐檯。這整個過程是同步完成的。
  • 服務人員一看沒有現成的東西了,跑去現作,並告訴我說:先去作別的,作好了我叫你的號。因而我開心的找了個座位刷微信,等叫到了個人號了取回來。這叫作非阻塞,整個過程是異步的,由於我還刷了微信思考了人生。

異步是非阻塞的,可是同步能夠是阻塞的也能夠是非阻塞的,取決於消費的資源。

異步編程的挑戰

異步編程的主要困難在於,構建程序的執行邏輯時是非線性的,這須要將任務流分解成不少小的步驟,再經過異步回調函數的形式組合起來。在異步大行其道的javascript界常常能夠看到不少層的});,簡單酸爽到妙趣橫生。這一節將討論一些經常使用的處理異步的技術手段。

回調函數地獄

開頭的那個例子使用了4層的嵌套回調函數,若是流程更加複雜的話,還須要嵌套更多,這不是一個好的實踐。並且以回調的方式組織流程,在視覺上並非很直白,咱們須要更加優雅的方式來解耦和組織異步流。

使用傳統的javascript技術,能夠展平回調層次,例如咱們能夠改寫以前的例子:

 1 var ohs = require('./anticorruption/OpenHostService');
 2 var localConvertingService = require('./services/LocalConverting');
 3 var remoteRepository = require('./repositories/BusinessData');
 4 var calculationService = require('./services/Calculation');
 5 
 6 function(req, res) {
 7     var userData = req.body;
 8 
 9     ohs.retrieveResource(userData, ohsCb);
10 
11     function ohsCb(err, rs1) {
12         if(err) {
13             // error handling
14         }
15         localConvertingService.unitize(rs1, convertingCb);
16     }
17 
18     function convertingCb(err, rs2) {
19         if(err) {
20             // error handling
21         }
22         remoteRepository.loadBusinessData(rs2, loadDataCb);
23     }
24 
25     function loadDataCb(err, bs1) {
26         if(err) {
27             // error handling
28         }
29         calculationService.doCalculation(bs1 , calclationCb);
30     }
31 
32     function calclationCb(err, result) {
33         if(err) {
34             // error handling
35         }
36         res.view(result);
37     }
38 }

解嵌套的關鍵在於如何處理函數做用域,以後金字塔厄運迎刃而解。

還有一種更爲優雅的javascript回調函數處理方式,能夠參考後面的Promise部分。

而對於像C#之類的內建異步支持的語言,那麼上述問題更加的不是問題,例如:

 1 public async IActionResult CrazyCase(UserData userData) {
 2     var ticket = CrazyApplication.Ticket;
 3 
 4     var ohsFactory = new OpenHostServiceFactory(ticket);
 5     var ohs = ohsFactory.CreateService();
 6 
 7     var ohsAdapter = new OhsAdapter(userData);
 8 
 9     var rs1 = await ohs.RetrieveResource(ohsAdapter);
10     var rs2 = await _localConvertingService.Unitize(rs1);
11     var bs1 = await _remoteRepository.LoadBusinessData(rs2);
12     var result = await _calculationService.DoCalculation(bs1);
13 
14     return View(result);
15 }

async/await這糖簡直不能更甜了,其它C#的編譯器仍是生成了使用TPL特性的代碼來作異步,說白了就是一些Task<T>在作後臺的任務,當遇到async/await關鍵字後,編譯器將該方法編譯爲狀態機,因此該方法就能夠在await的地方掛起和恢復了。整個的開發體驗幾乎徹底是同步式的思惟在作異步的事兒。後面有關於TPL的簡單介紹。

異常處理

因爲異步執行採用非阻塞的方式,因此當前的執行線程在調用後捕獲不到異步執行棧,所以傳統的異步處理將再也不適用。舉兩個例子:

1 try {
2     Task.Factory.StartNew(() => {
3         throw new InvalidOperationException("diablo coming.");
4     });
5 } catch(InvalidOperationException e) {
6     // nothing captured.
7     throw;
8 }

1 try {
2     process.nextTick(function() {
3         throw new Error('diablo coming.');
4     });
5 } catch(e) {
6     // nothing captured.
7     throw e;
8 }

在這兩個例子中,try語句塊中的調用會當即返回,不會觸發catch語句。那麼如何在異步中處理異常呢?咱們考慮異步執行結束後會觸發回調函數,那麼這即是處理異常的最佳地點。node的回調函數幾乎老是接受一個錯誤做爲其首個參數,例如:

fs.readFile('file.txt', 'utf-8', function(err, data) { });

編譯器所構建的狀態機能夠支持異常的處理,簡直是強大到無與倫比。固然,對於TPL的處理也有其專屬的支持,相似於node的處理方式:

1 Task.Factory.StartNew(() => {
2     throw new InvalidOperationException("diablo coming.");
3 })
4 .ContinueWith(parent => {
5     var parentException = parent.Exception;
6 });

注意這裏訪問到的parent.Exception是一個AggregateException類型,對應的處理方式也較傳統的異常處理也稍有不一樣:

1 parentException.Handle(e => {
2     if(e is InvalidOperationException) {
3         // exception handling.
4         return true;
5     }
6 
7     return false;
8 });

異步流程控制

異步的技術也許明白了,可是遇到更復雜的異步場景呢?假設咱們須要異步並行的將目錄下的3個文件讀出,所有完成後進行內容拼接,那麼就須要更細粒度的流程控制。

咱們能夠借鑑async.js這款優秀的異步流程控制庫所帶來的便捷。

 1 async.parallel([
 2     function(callback) {
 3          fs.readFile('f1.txt', 'utf-8', callback)
 4     },
 5     function(callback) {
 6          fs.readFile('f2.txt', 'utf-8', callback)
 7     },
 8     function(callback) {
 9          fs.readFile('f3.txt', 'utf-8', callback)
10     }
11 ], function (err, fileResults) {
12     // concat the content of each files
13 });

若是使用C#並配合TPL,那麼這個場景能夠這麼實現:

 1 public async void AsyncDemo() {
 2     var files = new []{
 3         "f1.txt",
 4         "f2.txt",
 5         "f3.txt"
 6     };
 7 
 8     var tasks = files.Select(file => {
 9         return Task.Factory.StartNew(() => {
10             return File.ReadAllText(file);
11         });
12     });
13 
14     await Task.WhenAll(tasks);
15 
16     var fileContents = tasks.Select(t => t.Result);
17 
18     // concat the content of each files
19 }

咱們再回到咱們開頭遇到到的那個場景,可使用async.jswaterfall來簡化:

 1 var ohs = require('./anticorruption/OpenHostService');
 2 var localConvertingService = require('./services/LocalConverting');
 3 var remoteRepository = require('./repositories/BusinessData');
 4 var calculationService = require('./services/Calculation');
 5 var async = require('async');
 6 
 7 function(req, res) {
 8     var userData = req.body;
 9 
10     async.waterfall([
11         function(callback) {
12             ohs.retrieveResource(userData, function(err, rs1) {
13                 callback(err, rs1);
14             });
15         },
16         function(rs1, callback) {
17             localConvertingService.unitize(rs1, function(err, rs2) {
18                 callback(err, rs2);
19             });
20         },
21         function(rs2, callback) {
22             remoteRepository.loadBusinessData(rs2, function(err, bs1) {
23                 callback(err, bs1);
24             });
25         },
26         function(bs1, callback) {
27             calculationService.doCalculation(bs1, function(err, result) {
28                 callback(err, result);
29             });
30         }
31     ],
32     function(err, result) {
33         if(err) {
34             // error handling
35         }
36         res.view(result);
37     });
38 }

若是須要處理先後無依賴的異步任務流可使用async.series()來串行異步任務,例如先開電源再開熱水器電源最後亮起紅燈,並無數據的依賴,但有前後的順序。用法和以前的parallel()waterfall()大同小異。另外還有優秀的輕量級方案step,以及爲javascript提供monadic擴展的wind.js(特別像C#提供的方案),有興趣能夠深刻了解。

反人類的編程思惟

異步是反人類的

人類生活在一個充滿異步事件的世界,可是開發者在構建應用時卻遵循同步式思惟,究其緣由就是由於同步符合直覺,而且能夠簡化應用程序的構建。

究其深層緣由,就是由於現實生活中咱們是在演繹,並經過不一樣的口頭回調來完成一系列的異步任務,咱們會說你要是有空了來找我聊人生,貨到了給我打電話,小紅你寫完文案了交給小明,小麗等全部的錢都到了通知小強……而在作開發時,咱們是在列清單,咱們的說法就是:我等着你有空而後開始聊人生,我等着貨到了而後我就知道了,我等着小紅文案寫完了而後開始讓她交給小明,我等着小麗確認全部的錢到了而後開始讓她通知小強……

同步的思惟能夠簡化編程的關注點,可是沒有將流程進行現實化的切分,咱們老是傾向於用同步阻塞的方式來將開發變成簡單的步驟程序化,卻忽視了用動態的視角以及消息/事件驅動的方式構建任務流程。

異步在編程看來是反人類的,可是從業務角度看倒是再合理不過的了。經過當的工具及技術,使用異步並非難以企及的,它可使應用的資源利用更加的高效,讓應用的響應性更上一個臺階。

擴展閱讀

Promise/Deferred

在通常狀況下,Promise、Deferred、Future這些詞能夠當作是同義詞,描述的是同一件事情。

jQuery 1.5+以後出現了一種新的API調用方式,相比於舊的API,新的方式更好的解耦了關注點,並帶來了更好的組合能力。

咱們看一個傳統的使用ajax的例子:

1 $.get('/api/service1', {
2     success: onSuccess,
3     failure: onFailure,
4     always:  onAlways
5 });

使用新的API後,調用的方式變成了:

1 $.get('/api/service1')
2     .done(onSussess)
3     .fail(onFailure)
4     .always(onAlways);

get方法返回的是一個promise對象,表示這個方法會在將來某個時刻執行完畢。

PromiseCommonJS提出的規範,而jQuery的實如今其基礎上有所擴展,旗艦級的實現能夠參考Kris KowalQ.js

咱們使用jQuery來構建一個promise對象:

 1 var longTimeOperation = function() {
 2     var deferred = $.Deferred();
 3 
 4     // taste like setTimeout()
 5     process.nextTick(function() {
 6         // do operation.
 7         deferred.resolve();
 8         // if need error handling, use deferred.reject();
 9     });
10 
11     return deferred.promise();
12 }
13 
14 $.when(longTimeOperation())
15     .done(success)
16     .fail(failure);

因爲jQuery生成的Deferred能夠自由的進行resolve()reject(),因此在返回時咱們使用.promise()生成不含這個兩方法的對象,從而更好的封裝邏輯。

那麼Promise究竟帶給咱們的便利是什麼?Promise表示在將來這個任務會成功或失敗,可使用1和0來表示,那麼開發者立刻就開始歡呼了,給我布爾運算我能撬動地球!因而,咱們能夠寫出以下的代碼:

1 $.when(uploadPromise, downloadPromise)
2     .done(function() {
3         // do animation.
4     });

對於開頭的那個例子咱們說過有着更優雅的解回調函數嵌套的方案,那就是使用promise,咱們來嘗試改寫開頭的那個例子:

 1 var ohs = require('./anticorruption/OpenHostService');
 2 var localConvertingService = require('./services/LocalConverting');
 3 var remoteRepository = require('./repositories/BusinessData');
 4 var calculationService = require('./services/Calculation');
 5 var $ = require('jquery');
 6 
 7 function(req, res) {
 8     var userData = req.body;
 9 
10     function deferredCallback(deferred) {
11         return function(err) {
12             if(err) {
13                 deferred.reject(err);
14             } else {
15                 var args = Array.prototype.slice.call(arguments, 1);
16                 deferred.resolve(args);
17             }
18         };
19     }
20 
21     function makeDeferred(fn) {
22         var deferred = $.Deferred();
23         var callback = deferredCallback(deferred);
24         fn(callback);
25         return deferred.promise();
26     }
27 
28     var retrieveResourcePromise = makeDeferred(function(callback) {
29         ohs.retrieveResource(userData, callback);
30     });
31 
32     var convertingPromise = makeDeferred(function(callback) {
33         localConvertingService.unitize(rs1, callback);
34     });
35 
36     var loadBusinessDataPromise = makeDeferred(function(callback) {
37         remoteRepository.loadBusinessData(rs2, callback);
38     });
39 
40     var calculationPromise = makeDeferred(function(callback) {
41         calculationService.doCalculation(bs1 , callback);
42     });
43 
44     var pipedPromise = retrieveResourcePromise
45         .pipe(convertingPromise)
46         .pipe(loadBusinessDataPromise)
47         .pipe(calculationPromise);
48 
49     pipedPromise
50         .done(function(result) {
51             res.view(result);
52         })
53         .fail(function(err) {
54             // error handling
55         });
56 }

咱們使用了一個高階函數來生成能夠兼容deferred構造的回調函數,進而使用jQuerypipe特性(在Q.js裏可使用then()組合每一個promise),使解決方案優雅了不少,而這個工具函數在Q.js裏直接提供,因而新的解決方案能夠以下:

 1 var ohs = require('./anticorruption/OpenHostService');
 2 var localConvertingService = require('./services/LocalConverting');
 3 var remoteRepository = require('./repositories/BusinessData');
 4 var calculationService = require('./services/Calculation');
 5 var Q = require('q');
 6 
 7 function(req, res) {
 8     var userData = req.body;
 9 
10     var retrieveResourceFn = Q.denodeify(ohs.retrieveResource)
11     var convertingFn = Q.denodeify(localConvertingService.unitize);
12     var loadBusinessDataFn = Q.denodeify(remoteRepository.loadBusinessData);
13     var calculationFn = Q.denodeify(calculationService.doCalculation);
14 
15     retrieveResourceFn(userData)
16         .then(convertingFn)
17         .then(loadBusinessDataFn)
18         .then(calculationFn)
19         .then(function(result) {
20             res.view(result);
21         }, function(err) {
22             // error handling
23         });
24 }

那咱們如何看待TPL特性呢?咱們看看TPL能夠作什麼:

  • Task爲基本構造單位,執行時不阻塞調用線程
  • 每一個Task是獨立的,Task有不一樣的狀態,可使用Task.Status獲取
  • Task能夠組合,使用相似.ContinueWith(Task))以及.WhenAll(Task[]).WhenAny(Task[])的方式自由組合。

對比一下Promise

  • Promise爲基本構造單位,表示一個未來完成的任務,調用時當即返回
  • 每一個Promise是獨立的,Promise有不一樣的狀態,可使用.state獲取
  • Promise能夠組合,使用.then().pipe()以及.when()來組合執行流程

能夠看到,不管是Promise仍是TPL,在設計上都有着驚人的類似性。咱們有理由猜測在其它的的語言或平臺都存在着相似的構造,由於異步說白了,就是讓將來完成的事情本身觸發後續的步驟。

Pull vs. Push

GoF32中沒有提到迭代器模式(Iterator)與觀察者模式(Observer)的區別和聯繫,其實這兩個模式有着千絲萬縷的聯繫。

Iterator反映的是一種Pull模型,數據經過同步的方式從生產者那裏拉過來,咱們經過它的定義即可看到這一事實:

1 interface IEnumerator<out T>: IDisposable
2 {
3     bool MoveNext();
4     T Current { get; }
5 }

經過阻塞的方式調用MoveNext(),數據一個一個的拉取到本地。

而Observer反映的是一種Push模型,經過註冊一個觀察者(相似於回調函數),當生產者有數據時,主動的推送到觀察者手裏。觀察者註冊結束後,本地代碼沒有阻塞,推送數據的整個過程是異步執行的。咱們經過它的定義來對比Iterator:

1 interface IObserver<in T>
2 {
3     void OnCompleted();
4     void OnError(Exception exception);
5     void OnNext(T value);
6 }

咱們發現,其實這兩個接口是徹底對偶的(參見Erik Meijer大大的論文Subject/Observer is Dual to Iterator):

  • MoveNext()拉取下一個數據,OnNext(T)推送下一個數據
  • MoveNext()返回值指示了有無剩餘數據(完成與否),OnCompleted()指示了數據已完成(推送數據完成的消息)
  • Iterator是同步的,因此出現了異常直接在當前運行棧上,Observer是異步的,因此須要另外一種方式來通知發生了異常(參見上文中的異步處理一節),因而有了OnError(Exception)

那麼事情就變的有意思了,咱們知道Enumerable的數據能夠任意的方式組合,因而產生了像LINQ之類的庫可供咱們使用,可是這是一種阻塞的方式,由於Iterator自己就是一種Pull模型,這造就了同步等待的結果。

沒錯你是對的,若是使用EF之類的框架來查詢數據庫,大部分的操做是延遲執行的,代表操做並無發生而是像佔位符同樣在那裏。可是別忘了,你最終須要去查詢數據庫的,在查詢的一剎那,世界仍是阻塞的,等結果吧親。

而Observer是異步Push的,有點像是事件驅動,有事件了觸發,沒事件了也不干擾訂閱者的執行。

你是否是也隱隱的以爲事件也能夠和Push模式同樣有統一的模型?並且不僅一次?

好,咱們重複一遍:事件,非阻塞觸發(並帶有事件數據)。Push,非阻塞通知訂閱者。

其實,這是同一種模式,語言中對事件(就是event關鍵字)的支持其實就是對Observer模式的支持,而foreach則實現了對Iterator模式的語言內建支持。所謂設計模式,就是由於語言的內建支持不夠而出現的,說白了,是語言的補丁。

那麼咱們來看一看異常強大的Rx如何改變事件。

1 // unitized event
2 var mouseDown = Observable
3     .FromEventPattern<MouseEventArgs>(this.myPictureBox, "MouseDown")
4     .Select(x =>x.EventArgs);
5 
6 // unitized APM model
7 var request = WebRequest.Create("http://www.shinetechchina.com");
8 var webRequest = Observable
9     .FromAsyncPattern<WebResponse>(request.BeginGetResponse, request.EndGetResponse);
 1 ar mouseDown = Observable
 2     .FromEventPattern<MouseEventArgs>(this.controlSource, "MouseDown")
 3     .Select(x => x.EventArgs.GetPosition(this));
 4 var mouseUp = Observable
 5     .FromEventPattern<MouseEventArgs>(this.controlSource, "MouseUp")
 6     .Select(x => x.EventArgs.GetPosition(this));
 7 var mouseMove = Observable
 8     .FromEventPattern<MouseEventArgs>(this.controlSource, "MouseMove")
 9     .Select(x => x.EventArgs.GetPosition(this));
10 var dragandDrop = 
11     from down in mouseDown
12     from move in mouseMove.StartWith(down).TakeUntil(mouseUp)
13     select new {
14         X = move.X - down.X,
15         Y = move.Y - down.Y
16     };
17 
18 dragandDrop.Subscribe(value =>
19 {
20     DesktopCanvas.SetLeft(this.controlSource, value.X);
21     DesktopCanvas.SetTop(this.controlSource, value.Y);
22 });

Rx也提供了javascript擴展,有興趣能夠深刻研究。

(完)

ar mouseDown =Observable.FromEventPattern<MouseEventArgs>(this.controlSource,"MouseDown").Select(x => x.EventArgs.GetPosition(this));var mouseUp =Observable.FromEventPattern<MouseEventArgs>(this.controlSource,"MouseUp").Select(x => x.EventArgs.GetPosition(this));var mouseMove =Observable.FromEventPattern<MouseEventArgs>(this.controlSource,"MouseMove").Select(x => x.EventArgs.GetPosition(this));var dragandDrop =from down in mouseDown from move in mouseMove.StartWith(down).TakeUntil(mouseUp)selectnew{ X = move.X - down.X, Y = move.Y - down.Y }; dragandDrop.Subscribe(value =>{DesktopCanvas.SetLeft(this.controlSource, value.X);DesktopCanvas.SetTop(this.controlSource, value.Y);});
相關文章
相關標籤/搜索