Hello World
Spiga

LINQ to SQL异步查询

2008-03-01 01:51 by 老赵, 25426 visits

异步操作是提高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委托类型的对象,当数据库的异步查询完成之后即会调用该委托,在这里使用匿名方法更合适。

这里的关键是用到了自己扩展的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消息应该转发给哪个下层对象呢?

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

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

45 条回复

  1. Zhuang miao
    *.*.*.*
    链接

    Zhuang miao 2008-03-01 02:05:00

    现在看老赵文章,明显有点迷糊

  2. 老赵
    admin
    链接

    老赵 2008-03-01 02:16:00

    @Zhuang miao
    是不是哪里写的不够清楚吗?

  3. 坏人
    *.*.*.*
    链接

    坏人 2008-03-01 02:26:00

    没有发现如此使用带来的明确好处。看起来,既没有提高页面响应的速度,也没有提高负载,同时也并没有降低消耗。

  4. 老赵
    admin
    链接

    老赵 2008-03-01 02:42:00

    @坏人
    好处我在之前的文章中谈过了:http://www.cnblogs.com/JeffreyZhao/archive/2008/02/24/use-async-operation-properly.html
    简单的说,就是靠释放线程来提高应用程序的吞吐能力,这样应用程序的负载自然就提高了,如果要一台服务器提高可承受的并发量,这是一个非常重要的做法。
    这个方法不光在.net里有,Java里的NIO也是同样的原理。

  5. 怪怪
    *.*.*.*
    链接

    怪怪 2008-03-01 07:10:00

    最后那段想看看你的见解, 搬板凳等下回了^^

  6. 李战
    *.*.*.*
    链接

    李战 2008-03-01 09:10:00

    小老赵好文章!

    异步操作的一个最主要特征就是当前线程不用等待耗时的操作,可以腾出空闲来做其他事情,等耗时操作完成时,我们的程序会在收到通知后再处理返回的结果。

    如果,这个耗时操作是由外部系统负责,如,输入输出硬件子系统,或网络中的其他计算机,就用不着消耗当前CPU资源。CPU就可以处理其他事情了。

    如果,这个耗时操作也会用到本机的CPU,但本机可能有多核,这样的处理也可以交由空闲的核来处理,也能提高整机CPU吞吐量。

    异步操作在计算机资源利用率不足的情况下,很自然就让多种操作并行起来。但如果在计算机资源已经超负荷的情况下,异步操作不但需要一个个排队等待,反而在管理和调度操作次序上会花去很多功夫,甚至出现象“内存垃圾爆满导致垃圾回收工累死”那样的,系统大面积停顿的惨状。

    不过,目前我们所遇到的应用大都没有充分发挥硬件的威力。所以,异步操作对于这样的系统来说是非常有用的。而且,异步操作还可以忙里偷闲输出一些友好信息,制造机器快速运行的假象,让我们的用户有个心理安慰嘛。

    当然,如果你的线程空闲下来也没什么可做的话,就没必要搞什么异步操作了,因为操作系统会把你自动挂起。你还不如抽空出来喝杯茶呢...

  7. jillzhang
    *.*.*.*
    链接

    jillzhang 2008-03-01 09:16:00

    当然,如果你的线程空闲下来也没什么可做的话,就没必要搞什么异步操作
    ------------------------------------------------------------
    UI线程怎么知道它有没有事,但有时还是要让它空闲下来的

  8. 李战
    *.*.*.*
    链接

    李战 2008-03-01 10:01:00

    @jillzhang
    嗯,既然是你的UI线程里发起了异步操作,用户就是你的老板,你当然要转头告诉老板“我在干活”,甚至还要随时向老板报告进度,就不能溜出去喝茶了。
    这里是老赵的地盘,我们私下打一架如何? 给你留言了

  9. Zhuang miao
    *.*.*.*
    链接

    Zhuang miao 2008-03-01 12:16:00

    非也,非也,是老赵技术提升太快,而我尚且原地停留~呵呵~还需努力

  10. 坏人
    *.*.*.*
    链接

    坏人 2008-03-01 12:26:00

    我想当然的理解是:即便你使用异步调用,线程应当仍旧在执行其他的页面执行周期,而不会释放进线程池,回头等数据读取好了之后再开过来继续操作,当然这只是我想当然的认为,如果不是这样,或许是可以提高一些负载能力的,那么,具体可以提升多少,你有做过实际的测试吗?我很有兴趣想了解下这个数字.

    另外,通常WEB前端的主要负载会是IO,而数据库则是IO与CPU计算的负载差不多,这样仅仅是提升了WEB前端的并发能力,而并没有实际的减少任何负载,那么我们可以认为这样的解决方式没有针对整个系统的瓶颈部分的话,效果会理想吗?而实际上引起高请求量的通常是资源部分,这部分一般会被单独剥离到一台或多台独立的服务器,并且一个数据库操作,我认为如果消耗太多时间,本身就不太合理,需要改进.

    我并不是在质疑什么,只是希望讨论下这个方式,无论效果如何,这算是一个比较独特的方式,即便效果不好,也值得尝试,总需要不断的尝试来找到更好的办法.

  11. 坏人
    *.*.*.*
    链接

    坏人 2008-03-01 12:29:00

    不过,目前我们所遇到的应用大都没有充分发挥硬件的威力。所以,异步操作对于这样的系统来说是非常有用的。而且,异步操作还可以忙里偷闲输出一些友好信息,制造机器快速运行的假象,让我们的用户有个心理安慰嘛。

    鉴与HTTP的一次性基本结构,你要想让他输出点友好信息,回头再来把正确的数据输出,恩,理论上是不可能的,恐怕你误解了这里使用异步的效果了,假如有效果的话.

  12. volnet(可以叫我大V)
    *.*.*.*
    链接

    volnet(可以叫我大V) 2008-03-01 13:34:00

    其实这同样适用于非LINQ的环境……
    或者说文章仅仅拿LINQ做了一个示例吧

  13. 老赵
    admin
    链接

    老赵 2008-03-01 14:01:00

    @坏人
    如果开始了异步操作,线程就被释放了,可用于处理其他请求。
    负载能够提高多少,要看具体应用所需要的逻辑,只能针对某个项目来说,没有办法说出明确的数字的——不过我有这方面的成功经验。
    至于你说的问题我可以来回答一下:如果每个请求都要去数据库进行(较长时间的)查询,那么异步查询可能没有太大效果。但在一般情况下,在系统运行期间,有相当的请求是不需要查询数据库(例如从缓存获取数据,或者只是计算,或者只是收集数据等待批量写入等等),或者数据库查询能够快速返回,将一些耗时操作进行异步,这样就可以处理更多上面这种“轻量请求”。
    异步操作是个补充,这和分离服务器,优化数据库并不是矛盾的,换句话说,优化数据库之后还是可以使用异步操作来做进一步优化。
    其实这个做法也已经被各种开发平台所接受了,并不是.net特有的。

  14. 老赵
    admin
    链接

    老赵 2008-03-01 14:07:00

    @volnet(可以叫我大V)
    异步操作和LINQ无关,我这篇文章也不是在谈论异步操作,也不是在用LINQ做示例来说明异步的好处。
    我这篇文章本身只是专注于如何将LINQ to SQL的查询异步化。

  15. 老赵
    admin
    链接

    老赵 2008-03-01 14:12:00

    对了,这里一直在说数据库的异步查询,这只是异步操作的一种。
    还有一种比较常见的需求就是从外网读取数据(比如图片,比如外部WebService),这个操作所花时间还不受我们控制,这种耗时操作则更需要异步了。

  16. 老赵
    admin
    链接

    老赵 2008-03-01 14:27:00

    --引用--------------------------------------------------
    Zhuang miao: 非也,非也,是老赵技术提升太快,而我尚且原地停留~呵呵~还需努力
    --------------------------------------------------------
    这些都是05年就有的东西啊,呵呵。

  17. 坏人
    *.*.*.*
    链接

    坏人 2008-03-01 16:10:00

    针对楼主上面的回复,我怎么觉得有点奇怪?

    周末晕呼了?呵呵,在这里使用异步,无非就是希望从外部(如数据库)获取资源的时间比较长的时候,能够把占用的线程释放后用于其他地方,以增加系统的可用线程数来提高吞吐能力吗?那么怎么可能会出现

    "如果每个请求都要去数据库进行(较长时间的)查询,那么异步查询可能没有太大效果。但在一般情况下,在系统运行期间,有相当的请求是不需要查询数据库(例如从缓存获取数据,或者只是计算,或者只是收集数据等待批量写入等等),或者数据库查询能够快速返回"

    的效果呢?呵呵,说反了吧,呵呵.

    恩,我也相信这和平台无关,只是对于这样做能带来多大的实际效果,我仍然持怀疑态度,原因还是我上面所说的,不过建议LZ如果希望推广该技术,希望此文章能影响大家使用该技术的话,分析列举出一些数据,恐怕比"没有办法说出明确的数字的——不过我有这方面的成功经验。"来得有说服力得多噢,只是个人建议,仅供参考:D

  18. 老赵
    admin
    链接

    老赵 2008-03-01 16:26:00

    @坏人
    如果每个请求都去数据库作长时间查询,那么即使用了异步操作,每个请求还是必须等待数据库返回(数据库本身限制了负载),性能提高不会太明显。但是因为有很多请求是“轻量的”,所以等待数据库操作的请求就可以把线程让给它们,那些轻量请求就可以快速返回了。否则大量轻量请求也需要等待长时间的数据库访问上了,问题在什么地方呢?
    其实很多东西(尤其是优化方面)也只能是在分享经验,例如说“XX做法能够提高XX效果”,然后把原因进行描述。即使我说了优化的数字,如果真是不相信的话,也可以认为这个数字是假的——而且很可能的确是不准确的,因为影响一个系统性能的东西有很多。所以我只能说我有这方面的成功经验,而真要说出这种做法有多少效果,我也只能说“要看项目本身”,因为的确“影响系统性能的因素有很多”。
    当然,我现在只能建议你可以自己尝试一下,呵呵。或者您也可以谈一下您认为没有效果的原因。:)

  19. 老赵
    admin
    链接

    老赵 2008-03-01 16:35:00

    --引用--------------------------------------------------
    坏人: 另外,通常WEB前端的主要负载会是IO,而数据库则是IO与CPU计算的负载差不多,这样仅仅是提升了WEB前端的并发能力,而并没有实际的减少任何负载,那么我们可以认为这样的解决方式没有针对整个系统的瓶颈部分的话,效果会理想吗?
    --------------------------------------------------------
    我举一个例子:
    线程池里只有1个可用线程,也就是说同时只能处理1个请求。
    目前有4个请求依次发起:
    1、一个重量级请求,其中数据库查询需要4秒才能返回。
    2-4:都是轻量级请求,不用查询数据库,需要花费1秒时间。

    如果第一个请求在查询数据库操作上没有异步,那么总共要花费:
    4 + 1 + 1 + 1 = 7秒才能处理完所有请求。

    但是,如果使用了数据库异步操作,第一个请求在请求数据库之后会立即释放线程,然后在查询数据库的4秒内,3个轻量级请求都处理完了。最后数据库查询结束,第一个请求处理完毕。那么现在总共只花费了4秒钟。

    这就是异步操作提高系统并发能力的原因。

  20. 坏人
    *.*.*.*
    链接

    坏人 2008-03-01 20:08:00

    恩,明白你的意思了,看问题的角度不一样而已,我在关心被异步的线程操作本身,你关注的是他的周边操作.

    我所说的是针对当前线程,如果他没有耗费时间的操作,比如一个消耗非常多的时间的SP执行,那么你对他实现异步,可以减少这个线程的占用时间,如果当前线程的消耗非常少,比如1MS以内,那么说实话,可能往返回调的开销时间会比直接执行更多,这是我的视角.

    你的视角是看的整体,你的意思是在此线程之外,如果有很多频繁且耗时小的请求往返,那么他们可能需要大量的线程来保证吞吐,这在AJAX使用得多使用得好的地方尤显如此.

    总上所述的话,我认为,是需要有足够多的消耗时长比较大的,比如500MS以上的请求,以及更多的更频繁的小型操作(数量为前者的10倍,消耗时间在10MS以内),大概会有一定的效果显现出来.

    但,我依旧认为,这样做,事实上,只能是锦上添花,并无法大幅度的提升系统负载能力(也就是你说的吞吐量,但还得是有效的,这个的平均常量就是所谓的负载能力),因为即便不这样做,加大的开销,我觉得也是很小的.

    很重要的一点,呵呵,我可不是来拆台的,我通常只对我感兴趣的主题很关注,对于自己既感兴趣又没有十足把握的话题,就特别关注了,这也是我频繁回复的原因,之所以希望你提供数据,一来,确实那样更有说服力,二来,我也希望你可以提供一个那样的数据,因为对于我认为的:"效果应当不明显"我也没有十足的把握,并非不相信你的话,而是对此仍然持怀疑态度.

  21. 坏人
    *.*.*.*
    链接

    坏人 2008-03-01 20:09:00

    我对提高WEB系统的伸缩性,这个话题比较有兴趣,特别是越来越多的AJAX\各种交互导致以前靠简单的静态化就可以解决的问题变得越来越复杂.

  22. 老赵
    admin
    链接

    老赵 2008-03-01 21:11:00

    @坏人
    有一点你说的没错,想数据库的异步查询在长时间的查询中比较合适,时间非常短的操作用异步的话性能反而会降低。
    还有其实一个缓存合理的系统,缓存命中率一般都可以在70%以上,这样的话我所谓的“轻量级”请求数量还是很明显的。
    当然,任何性能上的优化是要靠发现问题+提出解决方案+验证解决方案的一系列过程来实现的,我这里也是根据我经验提出一个可行的办法。至于是否适合某个项目,试了才知道,呵呵。
    // 不如交换个msn,有机会多交流?:)

  23. 坏人
    *.*.*.*
    链接

    坏人 2008-03-02 13:08:00

    其实我并不是反对这样做,我也比较乐意尝试一些新的方式来提高负载,毕竟,WEB均衡、资源分离、读写分离、缓存甚至内存前端数据库、静态、等老几样,翻来覆去的就那么些名堂了,只是我觉得这个办法可能收效甚微而已,当然了,我也只是凭知觉猜测而已,并不确定。

    我的MSN是terry8750 # hotmail.com

  24. 一瞬间
    *.*.*.*
    链接

    一瞬间 2008-03-03 10:39:00

    赵老师,
    这里
    this.m_dataContext.BeginExecuteQuery(query, cb, state);好像少了一个参数。

  25. 老赵
    admin
    链接

    老赵 2008-03-03 10:46:00

    @一瞬间
    没错,多谢提醒。:)

  26. zeroone
    *.*.*.*
    链接

    zeroone 2008-03-09 21:30:00

    每次来看,都受益匪浅。

  27. yjmyzz@126.com[未注册用户…
    *.*.*.*
    链接

    yjmyzz@126.com[未注册用户] 2008-03-12 08:58:00

    赵老师能否抽空写一篇如何建立高性能web网站的文章,现在这方面的文章非常少,零星的只有一些生成静态,异步操作。。。之类

  28. 光影传说
    *.*.*.*
    链接

    光影传说 2008-03-13 20:53:00

    @Jeffrey Zhao
    这样的处理是很好,是可以提高Web访问的并发吞吐量,特别是需要长时间操作的,对于有应用服务器的,Web客户端只是一个简单的壳的这种程序,我感觉更为合适。老赵举的例子,是比较简单的,而且逻辑层跟Web客户端在一起,可能会感觉提升不大,只是优化了数据访问。送花........

  29. liubo_ethos[未注册用户]
    *.*.*.*
    链接

    liubo_ethos[未注册用户] 2008-04-29 15:34:00

    terryLee口中的牛人。。。

  30. 老赵
    admin
    链接

    老赵 2008-04-29 15:43:00

    ethos是个好地方啊……

  31. 银河使者
    *.*.*.*
    链接

    银河使者 2008-07-13 21:38:00

    就象ajax的XMLHttpRequest,向后台发送请求时,最好使用异步方式,要不客户端可能会感觉运行的不太流畅

  32. 老赵
    admin
    链接

    老赵 2008-07-13 22:28:00

    --引用--------------------------------------------------
    银河使者: 就象ajax的XMLHttpRequest,向后台发送请求时,最好使用异步方式,要不客户端可能会感觉运行的不太流畅
    --------------------------------------------------------
    完全不是这回事……异步数据库访问和AJAX带来的用户体验完全没有关系。

  33. whzncutt[未注册用户]
    *.*.*.*
    链接

    whzncutt[未注册用户] 2009-07-08 12:09:00

    赵老师:
    我试验了一下你这种基于IOCP的APM的编程模型,编码稍有不同;
    但在我的双核笔记本上(cpu1.66G),性能没什么变化。不知道是我代码的原因,还是机器cpu还是“空不出来”
    以下分别是同步和异步的测试页面,数据库连到另一台服务器上
    同步页面:
    public partial class SyncPage : System.Web.UI.Page
    {
    Stopwatch watch = null;
    public SqlConnection Connection = new SqlConnection();
    void OpenConnection()
    {
    if (Connection.State != ConnectionState.Open)
    {
    Connection.ConnectionString = ConfigurationManager.AppSettings["DBConnection"];
    Connection.Open();
    }
    }

    protected void Page_Load(object sender, EventArgs e)
    {
    watch = Stopwatch.StartNew();
    string query = "select top 10 name from table1 waitfor delay '00:00:06'";
    SqlCommand command = new SqlCommand(query, Connection);
    this.OpenConnection();
    this.rptItems.DataSource = command.ExecuteReader();
    this.rptItems.DataBind();
    Connection.Close();
    int random = 0;
    for (int i = 0; i < 500000; i++)
    {
    random = new Random((int)DateTime.Now.Ticks).Next();
    }
    }


    protected override void Render(HtmlTextWriter writer)
    {
    watch.Stop();
    lblTest.Text = "Elapsed:" + watch.Elapsed.TotalMilliseconds;
    base.Render(writer);
    }
    }

  34. whzncutt[未注册用户]
    *.*.*.*
    链接

    whzncutt[未注册用户] 2009-07-08 12:11:00

    class ItemDataContext
    {
    public SqlConnection Connection = new SqlConnection();
    void OpenConnection()
    {
    if (Connection.State != ConnectionState.Open)
    {
    Connection.ConnectionString = ConfigurationManager.AppSettings["DBConnection"];
    Connection.Open();
    }
    }
    public IAsyncResult BeginExecuteQuery( string query, AsyncCallback callback, object asyncState)
    {
    //SqlCommand command = dataContext.GetCommand(query, withNoLock);
    SqlCommand command = new SqlCommand(query, Connection);
    this.OpenConnection();

    AsyncResult<SqlDataReader> asyncResult =
    new AsyncResult<SqlDataReader>(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;
    }


    public List<string> EndExecuteQuery(IAsyncResult ar)
    {
    AsyncResult<SqlDataReader> asyncResult = (AsyncResult<SqlDataReader>)ar;

    if (!asyncResult.IsCompleted)
    {
    asyncResult.AsyncWaitHandle.WaitOne();
    }

    if (asyncResult.Exception != null)
    {
    throw asyncResult.Exception;
    }

    using (SqlDataReader reader = asyncResult.Result)
    {
    return this.Translate(reader).ToList();
    }
    }

    List<string> Translate(SqlDataReader reader)
    {
    List<string> list = new List<string>();
    while (reader.Read())
    {
    list.Add((string)reader["groupname"]);
    }
    return list;
    }

    }

  35. whzncutt[未注册用户]
    *.*.*.*
    链接

    whzncutt[未注册用户] 2009-07-08 12:11:00

    异步页面(AsyncResult<T>是照搬你的代码 )
    public partial class AsyncPage : System.Web.UI.Page
    {
    Stopwatch watch = null;
    protected void Page_Load(object sender, EventArgs e)
    {
    watch = Stopwatch.StartNew();
    this.AddOnPreRenderCompleteAsync(
    new BeginEventHandler(BeginAsyncOperation),
    new EndEventHandler(EndAsyncOperation));
    int random = 0;
    for (int i = 0; i < 500000; i++)
    {
    random = new Random((int)DateTime.Now.Ticks).Next();
    }

    }

    protected override void Render(HtmlTextWriter writer)
    {
    watch.Stop();
    lblTest.Text = "Elapsed:" + watch.Elapsed.TotalMilliseconds;
    base.Render(writer);
    }

    private ItemDataContext m_dataContext;

    private IAsyncResult BeginAsyncOperation(object sender, EventArgs e,
    AsyncCallback cb, object state)
    {
    this.m_dataContext = new ItemDataContext();
    string query = "select top 10 groupname from GRP_Group waitfor delay '00:00:06'";
    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();
    }
    }

    }

  36. 老赵
    admin
    链接

    老赵 2009-07-08 12:17:00

    异步处理,IOCP等等都不是为了单个请求的性能而提出的,它是为了“大并发”来的,所以说提高了“吞吐量”。
    使用异步,可以让服务器同时处理更多请求,所以应用程序的性能提高了。
    是这个意思。

  37. whzncutt[未注册用户]
    *.*.*.*
    链接

    whzncutt[未注册用户] 2009-07-08 12:24:00

    恩,您的意思我明白;不过理论上如果IOCP真的有效,在双核电脑上运行这样的单个请求页面应该也有些效果。毕竟我查数据库时waitfor了6秒; 这段时间cpu应该有空闲可以处理页面上for循环中的耗时操作; 6秒钟就算是包括了额外线程的开销及cpu的调度时间,也应该能空出不少时间吧?

  38. 老赵
    admin
    链接

    老赵 2009-07-08 12:36:00

    @whzncutt
    你理解错异步处理的意思了,这和单核多核武馆。
    事实上对于单个请求来说,你可以认为依旧是线性执行的,不会并行执行。
    甚至说,因为异步处理需要额外的调度,因此单个请求的性能甚至还略低于同步处理。
    所以异步处理提升的是整个应用程序的性能,不是单个请求的性能。

  39. whzncutt[未注册用户]
    *.*.*.*
    链接

    whzncutt[未注册用户] 2009-07-08 12:36:00

    不过也是,双核毕竟不是物理上的。。。
    想知道如果物理上是多核,那就算对于单请求,像我这样的测试场景应该也有不少性能上的提升吧!

  40. 老赵
    admin
    链接

    老赵 2009-07-08 12:41:00

    @whzncutt
    不会有提高的。我觉得你需要再理解一下异步请求的执行方式和作用,自己就可以得出结论了。

  41. whzncutt[未注册用户]
    *.*.*.*
    链接

    whzncutt[未注册用户] 2009-07-08 13:50:00

    又看了一遍《Lab:体会ASP.NET异步处理请求的效果》,不过这次体会不是很深。
    调试了一下代码,发现是自己代码的问题。确实是不清楚异步请求的执行方式. Page_Load里执行this.AddOnPreRenderCompleteAsync(new BeginEventHandler(BeginAsyncOperation),new EndEventHandler(EndAsyncOperation)),在Load方法里怎么会有异步。把for的费时操作挪到BeginAsyncOperation里,实现了我要的效果(异步的页面确实快,因为操作数据库时,cpu去执行了for操作),而且这个场景与多核关系不大!

    谢谢赵老师的文章,就是没有经历都试验一把。

  42. 老赵
    admin
    链接

    老赵 2009-07-08 14:06:00

    @whzncutt
    不过我好像还是觉得你哪里理解错了……建议多做几个实验,不要猜测,把原因都理清楚。你的代码有些乱,我也没有仔细看,呵呵。

  43. 链接

    Terry Sun 2010-04-21 13:57:24

    关于文中的BeginExecuteQuery方法我有个疑问,请老赵看一下
    当执行command.BeginExecuteReader,BeginExecuteQuery方法后将立即返回asyncResult对象,并将当前线程返回到CLR线程池,后又马上从CLR线程池中找到一个可以用线程分配给command.BeginExecuteReader的匿名回调函数并运行这个匿名函数,当遇到command.EndExecuteReader(ar)后阻塞这个正在运行的线程直到sqlcommand运行完成。
    如果是这样的话跟同步调用又有何区别,因为总会有一个CLR线程池中的线程在等待操作完成

  44. 链接

    Terry Sun 2010-04-21 16:43:18

    搞明白了,Begin方法中的AsyncCallback在Begin方法执行后不会立即执行,而是要等到I/O操作执行完以后才会调用AsyncCallback, 但是如果这个IO操作需要一段时间才能执行完的话客户端处于什么样的状态? 一直连接着服务端等着Response吗?

  45. 老赵
    admin
    链接

    老赵 2010-04-21 20:14:44

    @Terry Sun: 但是如果这个IO操作需要一段时间才能执行完的话客户端处于什么样的状态? 一直连接着服务端等着Response吗?

    没错

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我