|
異步操作是提高Web應(yīng)用程序吞吐量的重要手段,關(guān)于這方面的話(huà)題已經(jīng)在前文《正確使用異步操作》中解釋過(guò)了。對(duì)于大多數(shù)互聯(lián)網(wǎng)應(yīng)用來(lái)說(shuō),性能瓶頸數(shù)據(jù)庫(kù)訪(fǎng)問(wèn)。換句話(huà)說(shuō),一個(gè)請(qǐng)求在數(shù)據(jù)庫(kù)操作上所花的時(shí)間往往是最多的——并且占總時(shí)間的90%以上。因此,當(dāng)Web應(yīng)用程序的吞吐量因?yàn)閿?shù)據(jù)庫(kù)操作的阻塞而受到影響的話(huà),我們可是嘗試使用異步數(shù)據(jù)庫(kù)操作來(lái)進(jìn)行優(yōu)化。
如果我們使用LINQ to SQL,在默認(rèn)情況下是無(wú)法實(shí)現(xiàn)異步查詢(xún)的,所有的操作都非常自然——異步是不自然的,因?yàn)樗堰B續(xù)的操作拆成了兩段。如果理解了《在LINQ to SQL中使用Translate方法以及修改查詢(xún)用SQL》一文中所提出的擴(kuò)展方法,使用LINQ to SQL實(shí)現(xiàn)數(shù)據(jù)庫(kù)的異步查詢(xún)的方法應(yīng)該就很容易想到了:借助SqlCommand對(duì)象來(lái)完成。
在.NET中實(shí)現(xiàn)一個(gè)此類(lèi)異步操作自然是按照標(biāo)準(zhǔn)的APM(Asynchronous Programming Model,異步編程模型)來(lái)開(kāi)發(fā)一對(duì)Begin和End方法。按照APM進(jìn)行開(kāi)發(fā)其實(shí)不是一件非常容易的事情,不過(guò)在.NET 2.0里,尤其是在.NET 3.5中的某個(gè)特性,開(kāi)發(fā)此類(lèi)功能就變得容易一些了——也就是說(shuō),這是個(gè)在.NET 1.x => .NET 2.0 => .NET 3.5的演變過(guò)程中都得到改進(jìn)的特性,猜出來(lái)是什么了嗎?沒(méi)錯(cuò),這個(gè)特性就是“匿名方法”。
匿名方法事實(shí)上基于委托,有了匿名方法這個(gè)特性,在一些本該使用委托的地方就可以直接定義一個(gè)函數(shù)了。這種做法在很多時(shí)候能夠減少相當(dāng)程度的代碼量,尤其是本來(lái)很難省去的一些“條條框框”。例如,我們現(xiàn)在需要對(duì)一個(gè)Article列表按評(píng)論數(shù)量進(jìn)行排序,并且在排序時(shí)可以指定升序或降序。如果沒(méi)有匿名方法,我們一般會(huì)這么做:
public void SortByCommentCount(List<Article> articleList, bool ascending){ // use the overloaded method: List<T>.Sort(Comparison<T> compare) ArticleComparison comparison = new ArticleComparison(ascending); articleList.Sort(new Comparison<Article>(comparison.Compare));}class ArticleComparison{ private bool m_ascending; public ArticleComparison(bool ascending) { this.m_ascending = ascending; } public int Compare(Article a, Article b) { return (a.CommentCount - b.CommentCount) * (this.m_ascending ? 1 : -1); }}
我們使用接受Comparison<T>作為參數(shù)的List<T>.Sort方法重載,如果沒(méi)有特別的要求,我們只需寫(xiě)一個(gè)靜態(tài)方法就可以了——只要方法簽名符合Comparision<Article>就行了。可惜在這里,我們需要寫(xiě)一個(gè)額外的類(lèi),因?yàn)槲覀冃枰L(fǎng)問(wèn)一個(gè)額外的參數(shù)ascending,而這個(gè)參數(shù)不能在一個(gè)獨(dú)立的Comparision<Article>委托中獲得。于是我們寫(xiě)了一個(gè)ArticleComparison類(lèi),它唯一的目的就是封裝ascending。如果我們每次使用Sort功能都要封裝一個(gè)類(lèi)的話(huà)編寫(xiě)的代碼也就太多了。但是如果我們有了匿名方法之后:
public void SortByCommentCount (List<Article> articleList, bool ascending){ articleList.Sort(delegate(Article a, Article b) { return (a.CommentCount - b.CommentCount) * (ascending ? 1 : -1); });}
很明顯,這種內(nèi)聯(lián)寫(xiě)法省去了額外的方法定義。而且更重要的是,匿名函數(shù)體內(nèi)部能夠訪(fǎng)問(wèn)到當(dāng)前堆棧中的變量——其實(shí)這點(diǎn)才是最重要的。事實(shí)上,匿名方法的實(shí)現(xiàn)原理正是由編譯器自動(dòng)生成了一個(gè)封裝類(lèi)。有了匿名方法這個(gè)特性,我們就可以使用非常優(yōu)雅的做法來(lái)實(shí)現(xiàn)一些輕量的委托。至于.NET 3.5里對(duì)于匿名方法的改進(jìn),主要在于引入了Lambda Expression:
public void SortByCommentCount(List<Article> articleList, bool ascending){ articleList.Sort((a, b) => (a.CommentCount - b.CommentCount) * (ascending ? 1 : -1));}
編譯器會(huì)將現(xiàn)在的代碼編譯成之前與之前匿名方法相似的IL代碼。.NET 3.5中LINQ的大量操作都以委托作為參數(shù),因此也正是因?yàn)橛辛薒amda Expression到委托的轉(zhuǎn)化,LINQ才能有如此威力。現(xiàn)在開(kāi)發(fā)一個(gè)APM操作就方便多了。我們現(xiàn)在來(lái)構(gòu)造一個(gè)擴(kuò)展,將LINQ to SQL的查詢(xún)異步化。首先是Begin方法(其中有些輔助方法以及參數(shù)的含義可以見(jiàn)之前的《在LINQ to SQL中使用Translate方法以及修改查詢(xún)用SQL》一文):
public static IAsyncResult BeginExecuteQuery( this DataContext dataContext, IQueryable query, bool withNoLock, AsyncCallback callback, object asyncState){ SqlCommand command = dataContext.GetCommand(query, withNoLock); dataContext.OpenConnection(); AsyncResult<DbDataReader> asyncResult = new AsyncResult<DbDataReader>(asyncState); command.BeginExecuteReader(ar => { try { asyncResult.Result = command.EndExecuteReader(ar); } catch (Exception e) { asyncResult.Exception = e; } finally { asyncResult.Complete(); if (callback != null) callback(asyncResult); } }, null); return asyncResult;}
在《正確使用異步操作》一文中我們已經(jīng)談過(guò)什么樣的異步操作是“有效”的,從文章的內(nèi)容我們不難得出一個(gè)結(jié)論,那就是我們無(wú)法使用托管代碼“自行”實(shí)現(xiàn)適合I/O-Bound Operation的異步操作。我們?yōu)镈ataContext擴(kuò)展的異步操作肯定是“封裝”了ADO.NET所提供的異步特性來(lái)完成。很顯然,我們需要獲得一個(gè)DbDataReader,因此我們調(diào)用會(huì)調(diào)用SqlCommand對(duì)象的BeginExecuteReader方法,該方法的第一個(gè)參數(shù)是一個(gè)AsyncCallback委托類(lèi)型的對(duì)象,當(dāng)數(shù)據(jù)庫(kù)的異步查詢(xún)完成之后即會(huì)調(diào)用該委托,在這里使用匿名方法更合適。
這里的關(guān)鍵是用到了自己擴(kuò)展的AsyncResult<T>類(lèi),該類(lèi)除了標(biāo)準(zhǔn)的IAsyncResult實(shí)現(xiàn)之外,還釋放出一個(gè)System.Exception類(lèi)型的Exception屬性和T類(lèi)型的Result屬性。這兩個(gè)屬性的作用可以從上面的代碼中看出:Result的作用是保留異步操作的結(jié)果,Exception的作用自然是臨時(shí)保存調(diào)用SqlCommand.EndExecuteReader方法之后捕獲到的異常。這兩個(gè)臨時(shí)保留的對(duì)象都是為了在EndExecuteQuery方法中作進(jìn)一步處理:
public static List<T> EndExecuteQuery<T>( this DataContext dataContext, IAsyncResult ar){ AsyncResult<DbDataReader> asyncResult = (AsyncResult<DbDataReader>)ar; if (!asyncResult.IsCompleted) { asyncResult.AsyncWaitHandle.WaitOne(); } if (asyncResult.Exception != null) { throw asyncResult.Exception; } using (DbDataReader reader = asyncResult.Result) { return dataContext.Translate<T>(reader).ToList(); }}
根據(jù)APM的規(guī)則,End方法將接受一個(gè)參數(shù),那就是Begin方法的返回值。因此我們可以在代碼中將其轉(zhuǎn)換成AsyncResult<DbDataReader>對(duì)象。按照規(guī)則,如果調(diào)用End方法時(shí)異步調(diào)用還沒(méi)有完成,則應(yīng)該阻塞當(dāng)前線(xiàn)程直到異步操作完畢,因此我們的代碼調(diào)用了AsyncWaitHandle的WaitOne方法——當(dāng)然,這里的寫(xiě)法和我們的具體實(shí)現(xiàn)方式有關(guān)(詳見(jiàn)下文中AsyncResult<T>的實(shí)現(xiàn)方法)。然后檢查Exception屬性,如果不為空則表明在執(zhí)行數(shù)據(jù)庫(kù)的異步操作時(shí)拋出了一場(chǎng),因此我們的End方法也將其繼續(xù)拋出。最后自然是根據(jù)獲得的DbDataReader對(duì)象,并借助DataContext的Translate方法生成對(duì)象。
至于AsyncResult<T>類(lèi)型的實(shí)現(xiàn)方法非常簡(jiǎn)單,我在這里將其簡(jiǎn)單貼出,就不多作什么解釋了。不過(guò)有一點(diǎn)顯而易見(jiàn),由于C# 3.0中的Automatic Property特性,代碼量比之前又能少了許多:
private class AsyncResult<T> : IAsyncResult{ public AsyncResult(object asyncState) { this.AsyncState = asyncState; this.IsCompleted = false; this.AsyncWaitHandle = new ManualResetEvent(false); } public object AsyncState { get; private set; } public WaitHandle AsyncWaitHandle { get; private set; } public bool CompletedSynchronously { get { return false; } } public bool IsCompleted { get; private set; } public void Complete() { this.IsCompleted = true; (this.AsyncWaitHandle as ManualResetEvent).Set(); } public T Result { get; set; } public Exception Exception { get; set; }}
那么現(xiàn)在就來(lái)試用一下。在《正確使用異步操作》中也提到過(guò),即使異步操作得到了IOCP支持,也必須正確使用這些異步操作才能真正得到效果。換句話(huà)說(shuō),我們必須在ASP.NET提供的幾個(gè)方面來(lái)使用異步功能。ASP.NET目前提供了三個(gè)可用于異步操作的地方:異步HttpModule,異步HttpHandler和異步Page,其中最常用的可能就是異步Page了。
public partial class AsyncPage : System.Web.UI.Page{ protected void Page_Load(object sender, EventArgs e) { this.AddOnPreRenderCompleteAsync( new BeginEventHandler(BeginAsyncOperation), new EndEventHandler(EndAsyncOperation)); } private ItemDataContext m_dataContext; private IAsyncResult BeginAsyncOperation(object sender, EventArgs e, AsyncCallback cb, object state) { this.m_dataContext = new ItemDataContext(); var query = (from item in this.m_dataContext.Items orderby item.ItemID select item).Skip(10).Take(10); return this.m_dataContext.BeginExecuteQuery(query, cb, state); } private void EndAsyncOperation(IAsyncResult ar) { using (this.m_dataContext.Connection) { this.rptItems.DataSource = this.m_dataContext.EndExecuteQuery(ar); this.rptItems.DataBind(); } }}
異步數(shù)據(jù)庫(kù)訪(fǎng)問(wèn)已經(jīng)變得非常容易了,即使是LINQ to SQL也能較輕松地地獲得這方面的支持。不過(guò)在實(shí)際開(kāi)發(fā)過(guò)程中我們可能還會(huì)遇到一點(diǎn)小問(wèn)題:我們的應(yīng)用程序是分層的,而異步數(shù)據(jù)庫(kù)訪(fǎng)問(wèn)是數(shù)據(jù)訪(fǎng)問(wèn)層的能力。而如果我們要在表現(xiàn)層(HttpModule、HttpHandler、Page都屬于表現(xiàn)層)使用異步方法,就需要讓業(yè)務(wù)邏輯也提供一系列的支持——可能只是過(guò)渡,可能又是更多。這方面對(duì)于單線(xiàn)程的業(yè)務(wù)邏輯對(duì)象來(lái)說(shuō)可能不是問(wèn)題,但是在某些架構(gòu)中,業(yè)務(wù)邏輯對(duì)象可能會(huì)被多個(gè)線(xiàn)程(請(qǐng)求)同時(shí)訪(fǎng)問(wèn)。但是既然要使用異步操作,就需要把一組Begin和End消息發(fā)送給同一個(gè)對(duì)象——在多線(xiàn)程共享一個(gè)業(yè)務(wù)邏輯對(duì)象的情況下,又該如何知道某個(gè)End消息應(yīng)該轉(zhuǎn)發(fā)給哪個(gè)下層對(duì)象呢?
這個(gè)話(huà)題我們下次再討論吧。
it知識(shí)庫(kù):LINQ to SQL異步查詢(xún),轉(zhuǎn)載需保留來(lái)源!
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請(qǐng)第一時(shí)間聯(lián)系我們修改或刪除,多謝。