在《正确使用异步操作》一文中我们已经谈过什么样的异步操作是“有效”的,从文章的内容我们不难得出一个结论,那就是我们无法使用托管代码“自行”实现适合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>(
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();
}
}
根据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消息应该转发给哪个下层对象呢?