.NET中的異步編程:不同與傳統(tǒng)的異步實(shí)現(xiàn)
我們都知道.NET要實(shí)現(xiàn)異步編程,尤其是傳統(tǒng)的異步實(shí)現(xiàn)方法,很容易出現(xiàn)錯(cuò)誤。所以有許多程序員就盡量避免使用異步編程,即使在知道應(yīng)該使用的情況下。今天我要告訴大家就是不同與傳統(tǒng)異步實(shí)現(xiàn)的心得異步實(shí)現(xiàn)方式Continuation Passing Style簡(jiǎn)稱CPS
首先,我們看看下面這個(gè)方法:
- public int Add(int a, int b)
- {
- return a + b;
- }
我們一般這樣調(diào)用它:
- Print(Add(5, 6))
- public void Print(int result)
- {
- Console.WriteLine(result);
- }
如果我們以CPS的方式編寫上面的代碼則是這個(gè)樣子:
- public void Add(int a, int b, Action<int> continueWith)
- {
- continueWith(a+b);
- }
- Add(5, 6, (ret) => Print(ret));
就好像我們將方法倒過(guò)來(lái),我們不再是直接返回方法的結(jié)果;我們現(xiàn)在做的是接受一個(gè)委托,這個(gè)委托表示我這個(gè)方法運(yùn)算完后要干什么,就是傳說(shuō)的continue。對(duì)于這里來(lái)說(shuō),Add的continue就是Print。
不僅是上面這樣的代碼示例。在一個(gè)方法中,在本語(yǔ)句后面執(zhí)行的語(yǔ)句都可以稱之為本語(yǔ)句的continue。
CPS 與 Async
那么可能有人要問(wèn),你說(shuō)這么多跟異步有什么關(guān)系么?對(duì),跟異步有很大的關(guān)系?;叵肷弦黄恼拢?jīng)典的異步模式都是一個(gè)以Begin開頭的方法發(fā)起異步請(qǐng)求,并且向這個(gè)方法傳入一個(gè)回調(diào)(callback),當(dāng)異步執(zhí)行完畢后該回調(diào)會(huì)被執(zhí)行,那么我們可以稱該回調(diào)為這個(gè)異步請(qǐng)求的continue:
- stream.BeginRead(buffer, 0, 1024, continueWith, null)
這又有什么用呢?那先來(lái)看看我們期望寫出什么樣子的異步代碼吧(注意,這是偽代碼,不要沒(méi)有看文章就直接粘貼代碼到vs運(yùn)行):
- var request = HttpWebRequest.Create("http://www.google.com");
- var asyncResult1 = request.BeginGetResponse(...); var response = request.EndGetResponse(asyncResult1);
- using(stream = response.GetResponseStream())
- {
- var asyncResult2 = stream.BeginRead(buffer, 0, 1024, ...);
- var actualRead = stream.EndRead(asyncResult2);
- }
對(duì),我們想要像同步的方式一樣編寫異步代碼,我討厭那么多回調(diào),特別是一環(huán)嵌套一環(huán)的回調(diào)。
參照前面對(duì)CPS的討論,在request.BeginGetResponse之后的代碼,都是它的continue,如果我能夠有一種機(jī)制獲得我的continue,然后在我執(zhí)行完畢之后調(diào)用continue該多好啊??上В珻#沒(méi)有像Scheme那樣的控制操作符call/cc獲取continue。
- var request = HttpWebRequest.Create("http://www.google.com");
- 標(biāo)識(shí)1 var asyncResult1 = request.BeginGetResponse(...);
- var response = request.EndGetResponse(asyncResult1);
- using(stream = response.GetResponseStream())
- {
- 標(biāo)識(shí)2 var asyncResult2 = stream.BeginRead(buffer, 0, 1024, ...);
- var actualRead = stream.EndRead(asyncResult2);
- }
思路貌似到這兒斷了。但是我們是否可以換個(gè)角度想想,如果我們能給上面這段代碼加上標(biāo)識(shí):在每個(gè)異步請(qǐng)求發(fā)起的地方都加一個(gè)標(biāo)識(shí),而標(biāo)識(shí)之后的部分就是continue。
當(dāng)執(zhí)行到 標(biāo)識(shí)1 時(shí),立即返回,并且記住本次執(zhí)行只執(zhí)行到了 標(biāo)識(shí)1,當(dāng)異步請(qǐng)求完畢后,它知道上次執(zhí)行到了 標(biāo)識(shí)1,那么這個(gè)時(shí)候就從標(biāo)識(shí)1的下一行開始執(zhí)行,當(dāng)執(zhí)行到標(biāo)識(shí)2時(shí),又遇到一個(gè)異步請(qǐng)求,立即返回并記住本次執(zhí)行到了標(biāo)識(shí)2,然后請(qǐng)求完畢后從標(biāo)識(shí)2的下一行恢復(fù)執(zhí)行。那么現(xiàn)在的任務(wù)就是如果打標(biāo)識(shí)以及在異步請(qǐng)求完畢后如何從標(biāo)識(shí)位置開始恢復(fù)執(zhí)行。
yield 與 異步
如果你熟悉C# 2.0加入的迭代器特性,你就會(huì)發(fā)現(xiàn)yield就是我們可以用來(lái)打標(biāo)識(shí)的東西??聪旅娴拇a:
- public IEnumerator<int> Demo()
- {
- //code 1
- yield return 1;
- //code 2
- yield return 2;
- //code 3
- yield return 3;
- }
經(jīng)過(guò)編譯會(huì)生成類似下面的代碼(偽代碼,相差很遠(yuǎn),只是意義相近,想要了解詳情的同學(xué)可以自行打開Reflector觀看):
- public IEnumerator<int> Demo()
- {
- return new GeneratedEnumerator();
- }
- public class GeneratedEnumerator
- {
- private int state = 0;
- private int currentValue = 0;
- public bool MoveNext()
- {
- switch(state)
- {
- case 0:
- //code 1
- currentValue = 1;
- state = 1;
- return true;
- case 1:
- //code 2
- currentValue = 2;
- state = 2;
- return true;
- case 2:
- //code 3
- currentValue = 3;
- state = 3;
- return true;
- default:return false;
- }
- }
- public int Current{get{return currentValue;}}
- }
對(duì),C#編譯器將其翻譯成了一個(gè)狀態(tài)機(jī)。yield return就好像做了很多標(biāo)記,MoveNext每調(diào)用一次,它就執(zhí)行下個(gè)yield return之前的代碼,然后立即返回。
好,現(xiàn)在打標(biāo)記的功能有了,我們?nèi)绾卧诋惒秸?qǐng)求執(zhí)行完畢后恢復(fù)調(diào)用呢?通過(guò)上面的代碼,你可能已經(jīng)想到了,我們這里恢復(fù)調(diào)用只需要再次調(diào)用一下MoveNext就行了,那個(gè)狀態(tài)機(jī)會(huì)幫我們處理一切。
那我們改造我們的異步代碼:
- public IEnumerator<int> Download()
- {
- var request = HttpWebRequest.Create("http://www.google.com");
- var asyncResult1 = request.BeginGetResponse(...);
- yield return 1;
- var response = request.EndGetResponse(asyncResult1);
- using(stream = response.GetResponseStream())
- {
- var asyncResult2 = stream.BeginRead(buffer, 0, 1024, ...);
- yield return 1;
- var actualRead = stream.EndRead(asyncResult2);
- }
- }
標(biāo)記打好了,考慮如何在異步調(diào)用完執(zhí)行一下MoveNext吧。
呵呵,你還記得異步調(diào)用的那個(gè)AsyncCallback回調(diào)么?也就是異步請(qǐng)求執(zhí)行完會(huì)調(diào)用的那個(gè)。如果我們向發(fā)起異步請(qǐng)求的BeginXXX方法傳入一個(gè)AsyncCallback,而這個(gè)回調(diào)里會(huì)調(diào)用MoveNext怎么樣?
- public IEnumerator<int> Download(Context context)
- {
- var request = HttpWebRequest.Create("http://www.google.com");
- var asyncResult1 = request.BeginGetResponse(context.Continue(),null);
- yield return 1;
- var response = request.EndGetResponse(asyncResult1);
- using(stream = response.GetResponseStream())
- {
- var asyncResult2 = stream.BeginRead(buffer, 0, 1024, context.Continue(),null);
- yield return 1;
- var actualRead = stream.EndRead(asyncResult2);
- }
- }
Continue方法的定義是:
- public class Context
- {
- //...
- private IEnumerator enumerator;
- public AsyncCallback Continue()
- {
- return (ar) => enumerator.MoveNext();
- }
- }
在調(diào)用Continue方法之前,Context類還必須保存有Download方法返回的IEnumerator,所以:
- public class Context
- {
- //...
- private IEnumerator enumerator;
- public AsyncCallback Continue()
- {
- return (ar) => enumerator.MoveNext();
- }
- public void Run(IEnumerator enumerator)
- {
- this.enumerator = enumerator;
- enumerator.MoveNext();
- }
- }
那調(diào)用Download的方法就可以寫成:
- public void Main()
- {
- Program p = new Program();
- Context context = new Context();
- context.Run(p.Download(context));
- }
除了執(zhí)行方式的不同外,我們幾乎就可以像同步的方式那樣編寫異步的代碼了。
- public class Context
- {
- private IEnumerator enumerator;
- public AsyncCallback Continue()
- {
- return (ar) => enumerator.MoveNext();
- }
- public void Run(IEnumerator enumerator)
- {
- this.enumerator = enumerator;
- enumerator.MoveNext();
- }
- }
- private void btnDownload_click(object sender,EventArgs e)
- {
- Context context = new Context();
- context.Run(Download(context));
- }
- private IEnumerator<int> Download(Context context)
- {
- var request = HttpWebRequest.Create("http://www.google.com");
- var asyncResult1 = request.BeginGetResponse(context.Continue(),null);
- yield return 1;
- var response = request.EndGetResponse(asyncResult1);
- using(stream = response.GetResponseStream()) {
- var asyncResult2 = stream.BeginRead(buffer, 0, 1024, context.Continue(),null);
- yield return 1;
- var actualRead = stream.EndRead(asyncResult2);
- }
- }
完整的代碼如下(為了更好的演示,我將下面代碼改為Winform版本):
不知道你注意到?jīng)]有,我們不僅可以順序的編寫異步代碼,連using這樣的構(gòu)造也可以使用了。如果你想更深入的理解這段代碼,推薦你使用Reflector查看迭代器最后生成的代碼。我在這里做一下簡(jiǎn)短的描述:
1、Context的Run調(diào)用時(shí)會(huì)調(diào)用Dowload方法,得到一個(gè)IEnumerator對(duì)象,我們將該對(duì)象保存在Context的實(shí)例字段中,以備后用
2、調(diào)用該IEnumerator對(duì)象的MoveNext方法,該方法會(huì)執(zhí)行到第一個(gè)yield return位置,然后返回,這個(gè)時(shí)候request.BeginGetResponse已經(jīng)調(diào)用,這個(gè)時(shí)候線程可以干其他的事情了。
3、在BeginGetResponse調(diào)用時(shí)我們通過(guò)Context的Continue方法傳入了一個(gè)回調(diào),該回調(diào)里會(huì)執(zhí)行剛才保存的IEnumerator對(duì)象的MoveNext方法。也就是在BeginGetResponse這個(gè)異步請(qǐng)求執(zhí)行完畢后,會(huì)調(diào)用MoveNext方法,控制流又回到Download方法,執(zhí)行到下一個(gè)yield return…… 以此類推。
總結(jié)
我們發(fā)現(xiàn)我們要的東西就是怎樣將順序風(fēng)格的代碼轉(zhuǎn)換為CPS方式,如何去尋找發(fā)起異步請(qǐng)求這行代碼的continue。由于C#提供了yield這種機(jī)制,C#編譯器會(huì)為其生產(chǎn)一個(gè)狀態(tài)機(jī),能夠?qū)⒖刂茩?quán)在調(diào)用代碼和被調(diào)用代碼之間交換。
【編輯推薦】