LINQ to SQL异步查询

30 篇文章 0 订阅

http://www.prg-cn.com/article-4419-1.html

异步操作是提高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列表按评论数量 进行排序,并且在排序时可以指定升序或降序。如果没有匿名方法,我们一般会 这么做:

  1. public void SortByCommentCount (List<Article> articleList, bool ascending)
  2. {
  3.   // use the overloaded method: List<T>.Sort(Comparison<T> compare)
  4.   ArticleComparison comparison = new ArticleComparison(ascending);
  5.   articleList.Sort(new Comparison<Article>(comparison.Compare));
  6. }
  7. class ArticleComparison
  8. {
  9.   private bool m_ascending;
  10.    public ArticleComparison(bool ascending)
  11.   {
  12.      this.m_ascending = ascending;
  13.   }
  14.   public int Compare (Article a, Article b)
  15.   {
  16.     return (a.CommentCount - b.CommentCount) * (this.m_ascending ? 1 : -1);
  17.   }
  18. }
复制代码

我们使用接受Comparison<T>作为参数的List<T>.Sort 方法重载,如果没有特别的要求,我们只需写一个静态方法就可以了 ——只要方法签名符合Comparision<Article>就行了。可惜在 这里,我们需要写一个额外的类,因为我们需要访问一个额外的参数ascending ,而这个参数不能在一个独立的Comparision<Article>委托中获得。于是 我们写了一个ArticleComparison类,它唯一的目的就是封装ascending。如果我 们每次使用Sort功能都要封装一个类的话编写的代码也就太多了。但是如果我们 有了匿名方法之后:

  1. public void SortByCommentCount
  2.    (List<Article> articleList, bool ascending)
  3. {
  4.    articleList.Sort(delegate(Article a, Article b)
  5.   {
  6.      return (a.CommentCount - b.CommentCount) * (ascending ? 1 : -1);
  7.   });
  8. }
复制代码

很明显,这种内联写法省去了额外的方法定义。而 且更重要的是,匿名函数体内部能够访问到当前堆栈中的变量——其 实这点才是最重要的。事实上,匿名方法的实现原理正是由编译器自动生成了一 个封装类。有了匿名方法这个特性,我们就可以使用非常优雅的做法来实现一些 轻量的委托。至于.NET 3.5里对于匿名方法的改进,主要在于引入了Lambda Expression:

  1. public void SortByCommentCount (List<Article> articleList, bool ascending)
  2. {
  3.    articleList.Sort((a, b) => (a.CommentCount - b.CommentCount) * (ascending ? 1 : -1));
  4. }
复制代码

编译器会将现在的代码编译成之前 与之前匿名方法相似的IL代码。.NET 3.5中LINQ的大量操作都以委托作为参数, 因此也正是因为有了Lamda Expression到委托的转化,LINQ才能有如此威力。现 在开发一个APM操作就方便多了。我们现在来构造一个扩展,将LINQ to SQL的查 询异步化。首先是Begin方法(其中有些辅助方法以及参数的含义可以见之前的 《在LINQ to SQL中使用Translate方法以及修改查询用SQL》一文):

  1. public static IAsyncResult BeginExecuteQuery(
  2.    thisDataContext dataContext, IQueryable query, bool withNoLock,
  3.   AsyncCallback callback, object asyncState)
  4. {
  5.    SqlCommand command = dataContext.GetCommand(query, withNoLock);
  6.   dataContext.OpenConnection();
  7.    AsyncResult<DbDataReader> asyncResult =
  8.     new AsyncResult<DbDataReader>(asyncState);
  9.    command.BeginExecuteReader(ar =>
  10.   {
  11.     try
  12.     {
  13.       asyncResult.Result = command.EndExecuteReader(ar);
  14.     }
  15.     catch (Exception e)
  16.     {
  17.       asyncResult.Exception = e;
  18.     }
  19.     finally
  20.     {
  21.        asyncResult.Complete();
  22.       if (callback != null) callback(asyncResult);
  23.     }
  24.   }, null);
  25.    return asyncResult;
  26. }
复制代码

在《正确使用异步操作》一文中我们已 经谈过什么样的异步操作是“有效”的,从文章的内容我们不难得出 一个结论,那就是我们无法使用托管代码“自行”实现适合I/O- Bound Operation的异步操作。我们为DataContext扩展的异步操作肯定是 “封装”了ADO.NET所提供的异步特性来完成。很显然,我们需要获 得一个DbDataReader,因此我们调用会调用SqlCommand对象的 BeginExecuteReader方法,该方法的第一个参数是一个AsyncCallback委托类型 的对象,当数据库的异步查询完成之后即会调用该委托,在这里使用匿名方法更 合适。

这里的关键是用到了自己扩展的AsyncResult<T>类,该类 除了标准的IAsyncResult实现之外,还释放出一个System.Exception类型的 Exception属性和T类型的Result属性。这两个属性的作用可以从上面的代码中看 出:Result的作用是保留异步操作的结果,Exception的作用自然是临时保存调 用SqlCommand.EndExecuteReader方法之后捕获到的异常。这两个临时保留的对 象都是为了在EndExecuteQuery方法中作进一步处理:

  1. public static List<T> EndExecuteQuery<T>(
  2.    thisDataContext dataContext, IAsyncResult ar)
  3. {
  4.    AsyncResult<DbDataReader> asyncResult =
  5.      (AsyncResult<DbDataReader>)ar;
  6.   if (! asyncResult.IsCompleted)
  7.   {
  8.      asyncResult.AsyncWaitHandle.WaitOne();
  9.   }
  10.   if (asyncResult.Exception != null)
  11.   {
  12.     throw asyncResult.Exception;
  13.   }
  14.   using (DbDataReader reader = asyncResult.Result)
  15.   {
  16.     return dataContext.Translate<T>(reader).ToList();
  17.   }
  18. }
复制代码

根据APM的规则,End方法将接受一个参数,那就是Begin方法的返回 值。因此我们可以在代码中将其转换成AsyncResult<DbDataReader>对象 。按照规则,如果调用End方法时异步调用还没有完成,则应该阻塞当前线程直 到异步操作完毕,因此我们的代码调用了AsyncWaitHandle的WaitOne方法 ——当然,这里的写法和我们的具体实现方式有关(详见下文中 AsyncResult<T>的实现方法)。然后检查Exception属性,如果不为空则 表明在执行数据库的异步操作时抛出了一场,因此我们的End方法也将其继续抛 出。最后自然是根据获得的DbDataReader对象,并借助DataContext的Translate 方法生成对象。

至于AsyncResult<T>类型的实现方法非常简单, 我在这里将其简单贴出,就不多作什么解释了。不过有一点显而易见,由于C# 3.0中的Automatic Property特性,代码量比之前又能少了许多:

  1. private class AsyncResult<T> : IAsyncResult
  2. {
  3.   public AsyncResult(object asyncState)
  4.   {
  5.      this.AsyncState = asyncState;
  6.     this.IsCompleted = false;
  7.     this.AsyncWaitHandle = new ManualResetEvent (false);
  8.   }
  9.   public object AsyncState { get; private set; }
  10.   public WaitHandle AsyncWaitHandle { get; private set; }
  11.   public bool CompletedSynchronously { get { return false; } }
  12.   public bool IsCompleted { get; private set; }
  13.    public void Complete()
  14.   {
  15.     this.IsCompleted = true;
  16.     (this.AsyncWaitHandle as ManualResetEvent).Set ();
  17.   }
  18.   public T Result { get; set; }
  19.   public Exception Exception { get; set; }
  20. }
复制代码

那么现在就来试用一下 。在《正确使用异步操作》中也提到过,即使异步操作得到了IOCP支持,也必须 正确使用这些异步操作才能真正得到效果。换句话说,我们必须在ASP.NET提供 的几个方面来使用异步功能。ASP.NET目前提供了三个可用于异步操作的地方: 异步HttpModule,异步HttpHandler和异步Page,其中最常用的可能就是异步 Page了。

  1. public partial class AsyncPage : System.Web.UI.Page
  2. {
  3.   protected void Page_Load(object sender, EventArgs e)
  4.   {
  5.      this.AddOnPreRenderCompleteAsync(
  6.       new BeginEventHandler(BeginAsyncOperation),
  7.       new EndEventHandler(EndAsyncOperation));
  8.   }
  9.   private ItemDataContext m_dataContext;
  10.   private IAsyncResult BeginAsyncOperation(object sender, EventArgs e,
  11.      AsyncCallback cb, object state)
  12.   {
  13.      this.m_dataContext = new ItemDataContext();
  14.     var query = (from item in this.m_dataContext.Items
  15.            orderby item.ItemID
  16.            select item).Skip (10).Take(10);
  17.     return this.m_dataContext.BeginExecuteQuery(query, cb, state);
  18.   }
  19.   private void EndAsyncOperation(IAsyncResult ar)
  20.   {
  21.     using (this.m_dataContext.Connection)
  22.     {
  23.        this.rptItems.DataSource = this.m_dataContext.EndExecuteQuery (ar);
  24.       this.rptItems.DataBind();
  25.     }
  26.   }
  27. }
复制代码

异步数据库访问已经变得非常容易了,即使是LINQ to SQL也能较轻松地地获得这方面的支持。不过在实际开发过程中我们可能还会遇 到一点小问题:我们的应用程序是分层的,而异步数据库访问是数据访问层的能 力。而如果我们要在表现层(HttpModule、HttpHandler、Page都属于表现层) 使用异步方法,就需要让业务逻辑也提供一系列的支持——可能只是 过渡,可能又是更多。这方面对于单线程的业务逻辑对象来说可能不是问题,但 是在某些架构中,业务逻辑对象可能会被多个线程(请求)同时访问。但是既然 要使用异步操作,就需要把一组Begin和End消息发送给同一个对象 ——在多线程共享一个业务逻辑对象的情况下,又该如何知道某个 End消息应该转发给哪个下层对象呢?

这个话题我们下次再讨论吧。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值