程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> LINQ to SQL異步查詢

LINQ to SQL異步查詢

編輯:關於.NET

異步操作是提高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(
   thisDataContext 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委托類型 的對象,當數據庫的異步查詢完成之後即會調用該委托,在這裡使用匿名方法更 合適。

這裡的關鍵是用到了自己擴展的AsyncResult<T>類,該類 除了標准的IAsyncResult實現之外,還釋放出一個System.Exception類型的 Exception屬性和T類型的Result屬性。這兩個屬性的作用可以從上面的代碼中看 出:Result的作用是保留異步操作的結果,Exception的作用自然是臨時保存調 用SqlCommand.EndExecuteReader方法之後捕獲到的異常。這兩個臨時保留的對 象都是為了在EndExecuteQuery方法中作進一步處理:

public static List<T> EndExecuteQuery<T>(
   thisDataContext 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();
  }
}

根據APM的規則,End方法將接受一個參數,那就是Begin方法的返回 值。因此我們可以在代碼中將其轉換成AsyncResult<DbDataReader>對象 。按照規則,如果調用End方法時異步調用還沒有完成,則應該阻塞當前線程直 到異步操作完畢,因此我們的代碼調用了AsyncWaitHandle的WaitOne方法 ——當然,這裡的寫法和我們的具體實現方式有關(詳見下文中 AsyncResult<T>的實現方法)。然後檢查Exception屬性,如果不為空則 表明在執行數據庫的異步操作時拋出了一場,因此我們的End方法也將其繼續拋 出。最後自然是根據獲得的DbDataReader對象,並借助DataContext的Translate 方法生成對象。

至於AsyncResult<T>類型的實現方法非常簡單, 我在這裡將其簡單貼出,就不多作什麼解釋了。不過有一點顯而易見,由於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; }
}

那麼現在就來試用一下 。在《正確使用異步操作》中也提到過,即使異步操作得到了IOCP支持,也必須 正確使用這些異步操作才能真正得到效果。換句話說,我們必須在ASP.NET提供 的幾個方面來使用異步功能。ASP.NET目前提供了三個可用於異步操作的地方: 異步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();
    }
  }
}

異步數據庫訪問已經變得非常容易了,即使是LINQ to SQL也能較輕松地地獲得這方面的支持。不過在實際開發過程中我們可能還會遇 到一點小問題:我們的應用程序是分層的,而異步數據庫訪問是數據訪問層的能 力。而如果我們要在表現層(HttpModule、HttpHandler、Page都屬於表現層) 使用異步方法,就需要讓業務邏輯也提供一系列的支持——可能只是 過渡,可能又是更多。這方面對於單線程的業務邏輯對象來說可能不是問題,但 是在某些架構中,業務邏輯對象可能會被多個線程(請求)同時訪問。但是既然 要使用異步操作,就需要把一組Begin和End消息發送給同一個對象 ——在多線程共享一個業務邏輯對象的情況下,又該如何知道某個 End消息應該轉發給哪個下層對象呢?

這個話題我們下次再討論吧。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved