使用非同步編程

来源:http://www.cnblogs.com/supernebula/archive/2016/02/11/5186413.html
-Advertisement-
Play Games

轉發至:http://www.ituring.com.cn/article/130823 導言 現代的應用程式面臨著諸多的挑戰,如何構建具有可伸縮性和高性能的應用成為越來越多軟體開發者思考的問題。隨著應用規模的不斷增大,業務複雜性的增長以及實時處理需求的增加,開發者不斷嘗試榨取硬體資源、優化。 在不


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

導言

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

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

開發者需要將不同的資源粘合在一起來提供最終的應用,這就需要協調不同的資源。

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

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

// 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);
}

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

線程是要增加開銷的,尤其是上下文的轉換,別扯什麼線程池了,創建線程的開銷是節省了,上下文切換的開銷才是要命的。

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


所以我們該怎麼辦?縱向擴展給伺服器加多點記憶體?橫向擴展上負載均衡?別鬧了我們又不是民工,想問題不要太簡單粗暴。解決的辦法就是,非同步,而且我們這篇也只討論非同步這一種技術。

為什麼使用非同步

那麼,非同步的優勢在哪裡?這首先要和同步做一個對比。

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

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[])的方式自由組合。

對比一下Pro

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 分類:C#、Android、VS2015;創建日期:2016-02-06 第4章 UI設計基礎 第3章雖然通過百度地圖應用展示了你可能感興趣的內容,但是,如果你是一個初學者,一開始就看懂和理解代碼可能會非常費勁。為瞭解決此問題,從這一章開始,本模塊將從最基本的內容講起,帶你逐步進入用C#進行Andr
  • 今天是大年初三,先跟大家拜個年,祝大家新年快樂。今天處理了一個alwaysOn問題——輔助副本因為磁碟空間不足一直顯示【未同步——可疑】,在日誌中可以看到資料庫處於掛起狀態,與主副本失去同步。原以為只需把輔助副本的磁碟做個清理,騰出一點空間,然後重啟SQL Server服務就好了(重啟讓資料庫從掛起...
  • 一、 1、現象:我們把資料庫的字元集編碼設置為utf-8,我們通過DOS界面向表的某一列插入漢字時會遇到類似 data too long for column 'name' at row 1 的錯誤。 2、錯誤原因: 3、解決的辦法: (1)set names gbk;(只對當前視窗有效) (2)找
  • 如何獲取 GemFire 8.2 安裝介質,以及在CentOS和Mac OS X的安裝過程。
  • -- 修改欄位 alter table emp MODIFY dept_id int; -- 刪除欄位 alter table emp drop COLUMN dept_id; 之前就當是 熱身了,跟著這個老師 你會覺得 真的能學到很多東西。要好好努力了! 上面兩個語句是通用性很強的語句。是 在 o
  • --1.sp_databas:列出伺服器上的所有資料庫信息,包括資料庫名稱和資料庫大小 exec sp_databases --2.sp_helpdb:報告有關指定資料庫或所有資料庫的信息 exec sp_helpdb --3.sp_renamedb:更改資料庫的名稱 exec sp_renamed
  • Nancy中Pipelines三兄弟(Before After OnError)的簡要概述以及多種用法。
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...