天天看點

親身體驗及舉例來全面解析C# 異步程式設計

當我們處理一些長線的調用時,經常會導緻界面停止響應或者iis線程占用過多等問題,這個時候我們需要更多的是用異步程式設計來修正這些問題,但是通常

都是說起來容易做起來難,誠然異步程式設計相對于同步程式設計來說,它是一種完全不同的程式設計思想,對于習慣了同步程式設計的開發者來說,在開發過程中難度更大,可控性

不強是它的特點。

在.net framework5.0種,微軟為我們系統了新的語言特性,讓我們使用異步程式設計就像使用同步程式設計一樣相近和簡單,本文中将會解釋以前版本的framework中基于回調道德異步程式設計模型的一些限制以及新型的api如果讓我們簡單的做到同樣的開發任務。

為什麼要異步

全不可控的問題,是以這也就要求程式員需要更多的去保護遠端資源的調用,管理調用的取消、超市、線程的等待以及處理線程長時間沒響應的情況等。而

在.net中我們通常忽略了這些挑戰,事實上我們會有多種不用的模式來處理異步程式設計,比如在處理io密集型操作或者高延遲的操作時候不組測線程,多數情況

我們擁有同步和異步兩個方法來做這件事。可是問題在于目前的這些模式非常容易引起混亂和代碼錯誤,或者開發人員會放棄然後使用阻塞的方式去開發。

而在如今的.net中,提供了非常接近于同步程式設計的程式設計體驗,不需要開發人員再去處理隻會在異步程式設計中出現的很多情況,異步調用将會是清晰的且不透明的,而且易于和同步的代碼進行組合使用。

過去糟糕的體驗

最好的了解這種問題的方式是我們最常見的一種情況:使用者界面隻擁有一個線程所有的工作都運作在這個線程上,用戶端程式不能對使用者的滑鼠時間做出反

應,這很可能是因為應用程式正在被一個耗時的操作所阻塞,這可能是因為線程在等待一個網絡id或者在做一個cpu密集型的計算,此時使用者界面不能獲得運作

時間,程式一直處于繁忙的狀态,這是一個非常差的使用者體驗。

很多年來,解決這種問題的方法都是做異步花的調用,不要等待響應,盡快的傳回請求,讓其他事件可以同時執行,隻是當請求有了最終回報的時候通知應用程式讓客戶代碼可以執行指定的代碼。

而問題在于:異步代碼完全毀掉了代碼流程,回調代了解釋了之後如何工作,但是怎麼在一個while循環裡等待?一個if語句?一個try塊或者一個using塊?怎麼去解釋“接下來做什麼”?

看下面的一個例子:

public int sumpagesizes(ilist<uri> uris) 

        { 

            int total = 0; 

            foreach (var uri in uris) 

            { 

                txtstatus.text = string.format("found {0} bytes...", total); 

                var data = new webclient().downloaddata(uri); 

                total += data.length; 

            } 

            txtstatus.text = string.format("found {0} bytes total", total); 

            return total; 

        } 

這個方法從一個uri清單裡下載下傳檔案,統計他們的大小并且同時更新狀态資訊,很明顯這個方法不屬于ui線程因為它需要花費非常長的時間來完成,這樣它會完全的挂起ui,但是我們又希望ui能被持續的更新,怎麼做呢?

我們可以建立一個背景程式設計,讓它持續的給ui線程發送資料來讓ui來更新自身,這個看起來是很浪費的,因為這個線程把大多時間花在等下和下載下傳上,但

是有的時候,這正是我們需要做的。在這個例子中,webclient提供了一個異步版本的downloaddata方法—

downloaddataasync,它會立即傳回,然後在downloaddatacompleted後觸發一個事件,這允許使用者寫一個異步版本的方法

分割所要做的事,調用立即傳回并完成接下來的ui線程上的調用,進而不再阻塞ui線程。下面是第一次嘗試:

public void sumpagesizesasync(ilist<uri> uris) 

            sumpagesizesasynchelper(uris.getenumerator(), 0); 

        public void sumpagesizesasynchelper(ienumerator<uri> enumerator, int total) 

            if (enumerator.movenext()) 

                var client = new webclient(); 

                client.downloaddatacompleted += (sender,e)=>{ 

                    sumpagesizesasynchelper(enumerator, total + e.result.length); 

                }; 

                client.downloaddataasync(enumerator.current); 

            else 

                txtstatus.text = string.format("found {0} bytes total", total); 

然後這依然是糟糕的,我們破壞了一個整潔的foreach循環并且手動獲得了一個enumerator,每一個調用都建立了一個事件回調。代碼用遞歸取代了循環,這種代碼你應該都不敢直視了吧。不要着急,還沒有完 。

原始的代碼傳回了一個總數并且顯示它,新的一步版本在統計還沒有完成之前傳回給調用者。我們怎麼樣才可以得到一個結果傳回給調用者,答案是:調用者必須支援一個回掉,我們可以在統計完成之後調用它。

然而異常怎麼辦?原始的代碼并沒有關注異常,它會一直傳遞給調用者,在異步版本中,我們必須擴充回掉來讓異常來傳播,在異常發生時,我們不得不明确的讓它傳播。

最終,這些需要将會進一步讓代碼混亂:

public void sumpagesizesasync(ilist<uri> uris,action<int,exception> callback) 

            sumpagesizesasynchelper(uris.getenumerator(), 0, callback); 

        public void sumpagesizesasynchelper(ienumerator<uri> enumerator, int total,action<int,exception> callback) 

            try 

                if (enumerator.movenext()) 

                { 

                    txtstatus.text = string.format("found {0} bytes...", total); 

                    var client = new webclient(); 

                    client.downloaddatacompleted += (sender, e) => 

                    { 

                        sumpagesizesasynchelper(enumerator, total + e.result.length,callback); 

                    }; 

                    client.downloaddataasync(enumerator.current); 

                } 

                else 

                    txtstatus.text = string.format("found {0} bytes total", total); 

                    enumerator.dispose(); 

                    callback(total, null); 

            catch (exception ex) 

                enumerator.dispose(); 

                callback(0, ex); 

當你再看這些代碼的時候,你還能立馬清楚的說出這是什麼jb玩意嗎?

恐怕不能,我們開始隻是想和同步方法那樣隻是用一個異步的調用來替換阻塞的調用,讓它包裝在一個foreach循環中,想想一下試圖去組合更多的異步調用或者有更複雜的控制結構,這不是一個subpagesizesasync的規模能解決的。

我們的真正問題在于我們不再可以解釋這些方法裡的邏輯,我們的代碼已經完全無章可循。異步代碼中很多的工作讓整件事情看起來難以閱讀并且似乎充滿了bug。

一個新的方式

如今,我們擁有了一個新的功能來解決上述的問題,異步版本的代碼将會如下文所示:

public async task<int> sumpagesizesasync(ilist<uri> uris) 

                var data = await new webclient().downloaddatataskasync(uri); 

除了添加的高亮的部分,上文中的代碼與同步版本的代碼非常相似,代碼的流程也從未改變,我們也沒有看到任何的回調,但是這并不代表實際上沒有回調操作,編譯器會搞定這些工作,不再需要您去關心。

異步的方法是用了task<int>替代了原來傳回的int類型,task和task<t>是在如今的framework提供的,用來代表一個正在運作的工作。

異步的方法沒有額外的方法,依照慣例為了差別同步版本的方法,我們在方法名後添加async作為新的方法名。上文中的方法也是異步的,這表示方法體會讓編譯器差別對待,允許其中的一部分将會變成回調,并且自動的建立task<int>作為傳回類型。

關于這個方法的解釋:在方法内部,調用另外一個異步方法downloaddatataskasync,它快速的傳回一個

task<byte[]>類型的變量,它會在下載下傳資料完成以後被激活,到如前為止,在資料沒有完成之前,我們不想做任何事,是以我們使用

await來等待操作的完成。

看起來await關鍵字阻塞了線程直到task完成下載下傳的資料可用,其實不然,相反它标志了任務的回調,并且立即傳回,當這個任務完成之後,它會執行回調。

tasks

task和task<t>類型已經存在于.net framework 4.0中,一個task代表一個進行時的活動,它可能是一個運作在單獨線程中的一個cpu密集型的工作或者一個io操作,手動的建立一個不工作在單獨線程的任務也是非常容易的:

static task readfileasync(string filepath,out byte[] buffer) 

            stream stream = file.open(filepath, filemode.open); 

            buffer = new byte[stream.length]; 

            var tcs = new taskcompletionsource<double>(); 

            stream.beginread(buffer, 0, buffer.length, arr => 

                var length = stream.endread(arr); 

                tcs.setresult(stream.length); 

            }, null); 

            return tcs.task; 

一旦建立了一個taskcompletionsource對象,你就可以傳回與它關聯的task對象,問相關的工作完成後,客戶代碼才得到最終的結果,這時task沒有占據自己的線程。 

如果實際任務失敗,task從樣可以攜帶異常并且向上傳播,如果使用await将觸發用戶端代碼的異常: 

static async void readassignedfile() 

            byte[] buffer; 

                double length = await readfileasync("somefiledonotexisted.txt", out buffer); 

                console.writeline(ex.message); 

        static task<double> readfileasync(string filepath,out byte[] buffer) 

                try 

                    var length = stream.endread(arr); 

                    tcs.setresult(stream.length); 

                catch (ioexception ex) 

                    tcs.setexception(ex); 

親身體驗及舉例來全面解析C# 異步程式設計

基于任務的異步程式設計模型

上文中解釋了異步方法應該是的樣子-task-based asynchronous pattern(tap),上文中異步的展現隻需要一個調用方法和異步異步方法,後者傳回一個task或者task<t>。

下文中将介紹一些tap中的約定,包括怎麼處理“取消”和“進行中”,我們将進一步講解基于任務的程式設計模型。

async和await

了解async方法不運作在自己的線程是非常重要的,事實上,編寫一個async方法但是沒有任何await的話,它就将會是一個不折不扣的同步方法:

static async task<int> tentosevenasync() 

    await task.delay(3000); 

    return 7; 

假如你調用這個方法,将會阻塞線程10秒後傳回7,這也許不是你期望的,在vs中也将得到一個警告,因為這可能永遠不是想要的結果。

隻有一個async方法運作到一個await語句時,它才立即把控制權傳回給調用方,然而隻有當等待的任務完成之後,它才會真正的傳回結果,這意味着你需要確定async方法中的代碼不會做過多的任務或者阻塞性能的調用。下面的執行個體才是你所期望的效果

task.delay實際上是異步版本的tread,sleep,它傳回一個task,這個task将會在指定的時間内完成。

時間處理程式和無傳回值的異步方法

異步方法可以從其他異步方法使用await建立,但是異步在哪裡結束?

在用戶端程式中,通常的回答是異步方法由事件發起,使用者點選一個按鈕,一個異步方法被激活,直到它完成,事件本身并不關系方法何時執行完成。這就是通常所說的“發後既忘”

為了适應這種模式,異步方法通常明确的被設計為“發後既忘”-使用void作為傳回值替代task<tresult>類型,這就讓方法

可以直接作為一個事件處理程式。當一個void saync的方法執行時,沒有task被傳回,調用者也無法追蹤調用是否完成。

private async void somebutton_click(object sender, routedeventargs e) 

    somebutton.isenabled = false; 

    await sumpagesizesasync(geturls())); 

    somebutton.isenabled = true; 

結束語

越寫到最後,越不說人話啦。。。。。

來源:51cto