天天看點

一起談.NET技術,.NET中的異步程式設計-Continuation passing style以及使用yield實作異步

  傳統的異步方式将本來緊湊的代碼都分成兩部分,不僅僅降低了代碼的可讀性,還讓一些基本的程式構造無法使用,是以大部分開發人員在遇到應該使用異步的地方都忍痛割愛。本來我在本篇文章中想讨論一下.NET世界中已有的幾個輔助異步開發的類庫,但是經過思考後覺得在這之前介紹一下一些理論知識也許對了解後面的類庫以及更新的内容有所幫助。今天我們要讨論的是Continuation Passing Style,簡稱CPS。

  首先,我們看看下面這個方法:

  我們一般這樣調用它:

  如果我們以CPS的方式編寫上面的代碼則是這個樣子:

  就好像我們将方法倒過來,我們不再是直接傳回方法的結果;我們現在做的是接受一個委托,這個委托表示我這個方法運算完後要幹什麼,就是傳說的continue。對于這裡來說,Add的continue就是Print。

不僅是上面這樣的代碼示例。在一個方法中,在本語句後面執行的語句都可以稱之為本語句的continue。

  那麼可能有人要問,你說這麼多跟異步有什麼關系麼?對,跟異步有很大的關系。回想上一篇文章,經典的異步模式都是一個以Begin開頭的方法發起異步請求,并且向這個方法傳入一個回調(callback),當異步執行完畢後該回調會被執行,那麼我們可以稱該回調為這個異步請求的continue:

  這又有什麼用呢?那先來看看我們期望寫出什麼樣子的異步代碼吧(注意,這是僞代碼,不要沒有看文章就直接粘貼代碼到vs運作):

  對,我們想要像同步的方式一樣編寫異步代碼,我讨厭那麼多回調,特别是一環嵌套一環的回調。

  參照前面對CPS的讨論,在request.BeginGetResponse之後的代碼,都是它的continue,如果我能夠有一種機制獲得我的continue,然後在我執行完畢之後調用continue該多好啊。可惜,C#沒有像Scheme那樣的控制操作符call/cc擷取continue。

  思路貌似到這兒斷了。但是我們是否可以換個角度想想,如果我們能給上面這段代碼加上辨別:在每個異步請求發起的地方都加一個辨別,而辨別之後的部分就是continue。

  當執行到 辨別1 時,立即傳回,并且記住本次執行隻執行到了 辨別1,當異步請求完畢後,它知道上次執行到了 辨別1,那麼這個時候就從辨別1的下一行開始執行,當執行到辨別2時,又遇到一個異步請求,立即傳回并記住本次執行到了辨別2,然後請求完畢後從辨別2的下一行恢複執行。那麼現在的任務就是如果打辨別以及在異步請求完畢後如何從辨別位置開始恢複執行。

  如果你熟悉C# 2.0加入的疊代器特性,你就會發現yield就是我們可以用來打辨別的東西。看下面的代碼:

  經過編譯會生成類似下面的代碼(僞代碼,相差很遠,隻是意義相近,想要了解詳情的同學可以自行打開Reflector觀看):

  對,C#編譯器将其翻譯成了一個狀态機。yield return就好像做了很多标記,MoveNext每調用一次,它就執行下個yield return之前的代碼,然後立即傳回。

  好,現在打标記的功能有了,我們如何在異步請求執行完畢後恢複調用呢?通過上面的代碼,你可能已經想到了,我們這裡恢複調用隻需要再次調用一下MoveNext就行了,那個狀态機會幫我們處理一切。

  那我們改造我們的異步代碼:

  标記打好了,考慮如何在異步調用完執行一下MoveNext吧。

  呵呵,你還記得異步調用的那個AsyncCallback回調麼?也就是異步請求執行完會調用的那個。如果我們向發起異步請求的BeginXXX方法傳入一個AsyncCallback,而這個回調裡會調用MoveNext怎麼樣?

  Continue方法的定義是:

  在調用Continue方法之前,Context類還必須儲存有Download方法傳回的IEnumerator,是以:

  那調用Download的方法就可以寫成:

  除了執行方式的不同外,我們幾乎就可以像同步的方式那樣編寫異步的代碼了。

  完整的代碼如下(為了更好的示範,我将下面代碼改為Winform版本):