LINQ to SQL異步查詢
[2] 異步查詢2
異步操作是提高Web應用程序吞吐量的重要手段,關于這方面的話題已經在前文《正確使用異步操作》中解釋過了。對于大多數互聯網應用來說,性能瓶頸數據庫訪問。換句話說,一個請求在數據庫操作上所花的時間往往是最多的——并且占總時間的90%以上。因此,當Web應用程序的吞吐量因為數據庫操作的阻塞而受到影響的話,我們可是嘗試使用異步數據庫操作來進行優化。
如果我們使用LINQ to SQL,在默認情況下是無法實現異步查詢的,所有的操作都非常自然——異步是不自然的,因為它把連續的操作拆成了兩段。如果理解了《在LINQ to SQL中使用Translate方法以及修改查詢用SQL》一文中所提出的擴展方法,使用LINQ to SQL實現數據庫的異步查詢的方法應該就很容易想到了:借助SqlCommand對象來完成。
在.NET中實現一個此類異步操作自然是按照標準的APM(Asynchronous Programming Model,異步編程模型)來開發一對Begin和End方法。按照APM進行開發其實不是一件非常容易的事情,不過在.NET 2.0里,尤其是在.NET 3.5中的某個特性,開發此類功能就變得容易一些了——也就是說,這是個在.NET 1.x => .NET 2.0 => .NET 3.5的演變過程中都得到改進的特性,猜出來是什么了嗎?沒錯,這個特性就是“匿名方法”。
匿名方法事實上基于委托,有了匿名方法這個特性,在一些本該使用委托的地方就可以直接定義一個函數了。這種做法在很多時候能夠減少相當程度的代碼量,尤其是本來很難省去的一些“條條框框”。例如,我們現在需要對一個Article列表按評論數量進行排序,并且在排序時可以指定升序或降序。如果沒有匿名方法,我們一般會這么做:
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>作為參數的List<T>.Sort方法重載,如果沒有特別的要求,我們只需寫一個靜態方法就可以了——只要方法簽名符合Comparision<Article>就行了。可惜在這里,我們需要寫一個額外的類,因為我們需要訪問一個額外的參數ascending,而這個參數不能在一個獨立的Comparision<Article>委托中獲得。于是我們寫了一個ArticleComparison類,它唯一的目的就是封裝ascending。如果我們每次使用Sort功能都要封裝一個類的話編寫的代碼也就太多了。但是如果我們有了匿名方法之后:
public void SortByCommentCount (List<Article> articleList, bool ascending) { articleList.Sort(delegate(Article a, Article b) { return (a.CommentCount - b.CommentCount) * (ascending ? 1 : -1); }); }
很明顯,這種內聯寫法省去了額外的方法定義。而且更重要的是,匿名函數體內部能夠訪問到當前堆棧中的變量——其實這點才是最重要的。事實上,匿名方法的實現原理正是由編譯器自動生成了一個封裝類。有了匿名方法這個特性,我們就可以使用非常優雅的做法來實現一些輕量的委托。至于.NET 3.5里對于匿名方法的改進,主要在于引入了Lambda Expression:
public void SortByCommentCount(List<Article> articleList, bool ascending) { articleList.Sort((a, b) => (a.CommentCount - b.CommentCount) * (ascending ? 1 : -1)); }
編譯器會將現在的代碼編譯成之前與之前匿名方法相似的IL代碼。.NET 3.5中LINQ的大量操作都以委托作為參數,因此也正是因為有了Lamda Expression到委托的轉化,LINQ才能有如此威力。現在開發一個APM操作就方便多了。我們現在來構造一個擴展,將LINQ to SQL的查詢異步化。首先是Begin方法(其中有些輔助方法以及參數的含義可以見之前的《在LINQ to SQL中使用Translate方法以及修改查詢用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; }
在《正確使用異步操作》一文中我們已經談過什么樣的異步操作是“有效”的,從文章的內容我們不難得出一個結論,那就是我們無法使用托管代碼“自行”實現適合I/O-Bound Operation的異步操作。我們為DataContext擴展的異步操作肯定是“封裝”了ADO.NET所提供的異步特性來完成。很顯然,我們需要獲得一個DbDataReader,因此我們調用會調用SqlCommand對象的BeginExecuteReader方法,該方法的第一個參數是一個AsyncCallback委托類型的對象,當數據庫的異步查詢完成之后即會調用該委托,在這里使用匿名方法更合適。