ntwo

导航

ASP.NET站点性能提升-线程

当网站访问文件或外部资源例如数据库或web service时,处理请求的线程需要等待这些资源响应,它会被阻塞。这个线程不能做任何事情,此时,可能有其它请求等待这个线程。

可以使用异步方法访问文件或外部资源解决这个问题。这些方法当开始等待时,就释放线程,而不是阻塞它,所以线程可以由其它请求使用。当资源空闲时,从线程池中获取一个新线程进行处理。

这章演示如果将阻塞线程的同步代码转换成异步代码。每个例子首先展示同步版本,然后,将看到如何将它变成异步版本。主要讨论:

  • 怎样转换访问web service的代码。
  • 怎样转换访问数据库的代码,包括怎样创建异步数据层。
  • 怎样转换访问文件的代码。
  • 怎样改变线程相关配置和页面超时。

异步web service访问

http://www.webservicex.net为例。

同步版本

  1. 在网站中加入web service。右键单击网站选择“添加web引用”,在URL字段中,输入web service地址:http://www.webservicex.net/WeatherForecast.asmx?WSDL。注意web service描述和命名空间:net.webservicex.www。
  2. 在代码中加入web service的命名空间:using net.webservicex.www;
  3. 实例化加入web引用时自动生成的代理类WeatherForecast:
    WeatherForecast weatherForecast = new WeatherForecast();
  4. 根据ZIP代码获取天气预报:
    WeatherForecasts weatherForecasts = weatherForecast.GetWeatherByZipCode(zip);
  5. 如果打开http://www.webservicex.net/WeatherForecast.asmx?WSDL。就会发现GetWeatherByZipCode方法返回的WeatherForecasts类包括一个日期列表和它们的预报。可以使用Repeater控件进行绑定:
    rpWeatherDetails.DataSource = weatherForecasts.Details;
    rpWeatherDetails.DataBind();
  6. Repeater的.aspx页面:
    <asp:Repeater ID="rpWeatherDetails" runat="server">
     <ItemTemplate>
    <p>
        <b><%# DataBinder.Eval(Container.DataItem, "Day")%></b><br />
        Min Temperature:
         <%# DataBinder.Eval(Container.DataItem, 
    "MinTemperatureF")%>F,
        Max Temperature: 
        <%# DataBinder.Eval(Container.DataItem, "MaxTemperatureF")%>F
    </p>
    </ItemTemplate>
    </asp:Repeater>

异步版本

首先,在.aspx文件中,设置Page指令的Async=”true” 并且设置AsyncTimeout属性。如果响应超时,运行时会调用定义的超时事件处理程序。

<%@ Page Language="C#" Async="true" AsyncTimeout="30" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default" %>

如果页面上有多点异步任务,超时时间是针对每个任务的。

如果不使用Page指令,也可以编程设置:

Page.AsyncTimeout = TimeSpan.FromSeconds(30);

然后,在代码中,注册PageAsyncTask对象。这个对象的构造器接收BeginAsync方法,这个方法开始web service访问。运行时,不会在注册后立即调用它,而是在pre-render页面事件后调用。构造器也会接收EndAsync方法,在操作完成后调用,TimeoutAsync在超时后调用。

可以使用构造器的第四个参数给BeginAsync方法传入额外的数据。最后,如果希望并行执行多个任务,一个一个注册它们,并且设置构造器的第五个参数为true。

protected void Page_Load(object sender, EventArgs e)
{
  PageAsyncTask pageAsyncTask = new PageAsyncTask(BeginAsync, EndAsync, TimeoutAsync, null, false);
  RegisterAsyncTask(pageAsyncTask);
}

一般情况下,在页面的OnLoad事件或其它事件中注册。如果需要在pre-render事件后注册,怎么办?如果需要根据一些任务的执行结果注册一个任务,怎么办?对于这些迟注册,使用ExecuteRegisteredAsyncTasks页面方法。这个方法手动运行所有已注册的,但是还未运行的任务。

在BeginAsync方法中,首先初始化创建web引用时自动生成的代理类WeatherForecast。然后,调用GetWeatherByZipCode web service方法的Begin版本。也是添加web引用时,Visual Studio自动生成的。

除了正常的GetWeatherByZipCode的ZIP参数,BeginGetWeatherByZipCode还接收BeginAsync传入的回调方法,和一个包括额外数据的对象。运行时将会传递给EndAsync。这里,传递weatherForecast代理对象,所以EndAsyn可以使用它调用EndGetWeatherByZipCode:

private IAsyncResult BeginAsync(object sender, EventArgs e,  
  AsyncCallback cb, object extraData)
{
  WeatherForecast weatherForecast = new WeatherForecast();
  IAsyncResult asyncResult =
  weatherForecast.BeginGetWeatherByZipCode(zip, cb,  
  weatherForecast);
  return asyncResult;
}

在EndAsync方法中,首先获得weatherForecast代理,它是通过额外数据传递给BeginAsync的。然后调用GetWeatherByZipCode web service方法的End..版本获取天气预报。最后,将天气预报绑定到repeater控件,和同步版本中的一样:

private void EndAsync(IAsyncResult asyncResult)
{
  object extraData = asyncResult.AsyncState;
  WeatherForecast weatherForecast = (WeatherForecast)extraData;
  WeatherForecasts weatherForecasts =
  weatherForecast.EndGetWeatherByZipCode(asyncResult);
  rpWeatherDetails.DataSource = weatherForecasts.Details;
  rpWeatherDetails.DataBind();
}

最后,TimeoutAsync方法响应超时,例如通过抛出一个异常警告访问者。这个方法和EndAsync方法接收相同的额外数据,所以可以访问在BeginAsync方法中传入的额外数据。

private static void TimeoutAsync(IAsyncResult asyncResult)
{
  // Timeout processing
}

异常版本的编程方式显示与正常的同步编程是不同的。首先,操作在pre-render事件后在BeginAsync方法中开始,而不是在页面的OnLoade事件中。而且也不是在调用web service方法后立即处理结果,处理过程在第二个方法EndAsync中进行。本质上,异步编程是处理事件,而不是编写顺序代码。

异步数据访问层

用法

为了保持简单性,异步数据层只访问一个包含书籍的数据库表,它只支持一个方法:根据bookId获取书籍。

不要忘记在使用异步操作的页面的Page指令中设置Async=”true”和设置AsnycTimeout。

<%@ Page Language="C#" Async="true" AsyncTimeout="30" AutoEventWireup="true"  CodeFile="Default.aspx.cs"  Inherits="_Default" %>

典型的访问异步数据层的代码片断如下:

DataAccessAsync.Books.GetById(bookId, delegate(BookDTO bookDTO)
{
  // This anonymous method will be called when the 
  // operation has been completed.
  Label1.Text = string.Format( 
    "Id: {0}, Title: {1}, Author: {2}, Price: {3}", 
    bookDTO.Id, bookDTO.Title, bookDTO.Author,  
    bookDTO.Price);
},
delegate()
{
  // This anonymous method will be called when a timeout happens.
  Label1.Text = "Timeout happened";
});

GetById数据访问方法注册根据书籍ID获取书籍的任务。它接收书籍ID,一个当数据获取后调用的方法,一个超时后调用的方法。这里,不定义分离的方法,其后传递给数据访问层方法,而是直接传递匿名方法,使得代码在一个地方。

实现

public delegate void OnFinishedGetByIdDelegate(BookDTO bookDTO);
public delegate void NoParamsDelegate();
public static void GetById(int bookId, OnFinishedGetByIdDelegate onFinished, NoParamsDelegate onTimedOut)
{
    AsyncData asyncData = AsyncDataForGetById(bookId, onFinished,nTimedOut);
    PageAsyncTask pageAsyncTask = new PageAsyncTask(BeginAsync, EndAsync, TimeoutAsync, asyncData, false);
    Page currentPage;
    currentPage = (Page)System.Web.HttpContext.Current.Handler;
    currentPage.RegisterAsyncTask(pageAsyncTask);
}

private class AsyncData
{
    public object onFinished { get; set; }
    public NoParamsDelegate onTimedOut { get; set; }
    public string commandText { get; set; }
    public List<SqlParameter> parameters { get; set; }
    public SqlCommand command { get; set; }
}

private static AsyncData AsyncDataForGetById(int bookId, OnFinishedGetByIdDelegate onFinished, NoParamsDelegate onTimedOut)
{
    AsyncData asyncData = new AsyncData();
    asyncData.onFinished = onFinished;
    asyncData.onTimedOut = onTimedOut;
    asyncData.commandText = "SELECT BookId, Author, Title, Price  
    FROM dbo.Book WHERE BookId=@BookId; ";
    List<SqlParameter> parameters = new List<SqlParameter>();
    parameters.Add(new SqlParameter("@BookId", bookId));
    asyncData.parameters = parameters;
    return asyncData;
}

private static IAsyncResult BeginAsync(object sender, EventArgs e, AsyncCallback cb, object extraData)
{
    AsyncData asyncData = (AsyncData)extraData;
    string connectionString = ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString;
    SqlConnection connection = new SqlConnection(connectionString);
connection.Open();
    SqlCommand command = new SqlCommand(asyncData.commandText, connection);
    foreach (SqlParameter sqlParameter in asyncData.parameters)
    {
        command.Parameters.Add(sqlParameter);
    }
    asyncData.command = command;
    IAsyncResult asyncResult = command.BeginExecuteReader(cb, asyncData);
    return asyncResult;
}

private static void EndAsync(IAsyncResult asyncResult)
{
    object extraData = asyncResult.AsyncState;
    AsyncData asyncData = (AsyncData)extraData;
    using (SqlCommand command = (SqlCommand)asyncData.command)
    {
        using (command.Connection)
        {
            using (SqlDataReader reader = command.EndExecuteReader(asyncResult))
            {
                // GetbyId specific processing
                OnFinishedGetByIdDelegate onFinishedGetById = asyncData.onFinished as OnFinishedGetByIdDelegate;
                if (onFinishedGetById != null)
                {
                    BookDTO bookDTO = null;
                    if (reader.Read())
                    {
                        bookDTO =new BookDTO(Convert.ToInt32(reader["BookId"]), reader["Author"].ToString(), reader["Title"].ToString(), Convert.ToDecimal(reader["Price"]));
                    }
                    onFinishedGetById(bookDTO);
                }
            }
        }
    }
}

private static void TimeoutAsync(IAsyncResult asyncResult)
{
    object extraData = asyncResult.AsyncState;
    AsyncData asyncData = (AsyncData)extraData;
    SqlCommand command = (SqlCommand)asyncData.command;
    command.Connection.Dispose();
    command.Dispose();
    if (asyncData.onTimedOut != null)
    {
        asyncData.onTimedOut();
    }
}

你可以对这段代码进行改进,例如将BeginAsync、EndAsync和TimeoutAsync方法放到通用的基类中,使得加入对不同类型的对象进行操作更简单。

性能测试

同步、异步版本差异依赖于使用IIS6、IIS7经典模式或IIS7集成模式,硬件、负载等。在II7集成模式下,异步版本的吞吐量是同步版本的至少5倍。

异步写文件

使用FileUpload控件上传文件后,写文件到磁盘使用异步代码,可以带来性能提升。

同步版本

protected void btnUpload_Click(object sender, EventArgs e)
{
  if (!FileUpload1.HasFile)
  {
    Label1.Text = "No file specified.";
    return;
  }
  string fileName = FileUpload1.FileName;
  FileUpload1.SaveAs(Server.MapPath(@"images\" + fileName));
}

异步版本

首先,在Page指令中加入Async=”true”。

<%@ Page Language="C#" Async="true" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>

然后,修改后端代码。很不幸,SaveAs方法没有异步版本,所以只好使用缓冲区保存文件内容,然后异步写入磁盘。

因为文件写入是在BeginAsync中初始化的,所以我们需要一个保存文件详细信息的类,这些信息就可以做为一个单独的对象传递给BeingAsync。

public class FileWriteData
{
  public byte[] fileBytesBuffer { get; set; }
  public int bufferLength { get; set; }
  public string fileName { get; set; }
  public FileStream fs { get; set; }
}

上传事件处理程序:

protected void btnUpload_Click(object sender, EventArgs e)
{
    if (!FileUpload1.HasFile)
    {
      Label1.Text = "No file specified.";
      return;
    }
    
    FileWriteData fwd = new FileWriteData();
    fwd.fileBytesBuffer = FileUpload1.FileBytes;
    fwd.bufferLength = FileUpload1.PostedFile.ContentLength;
    fwd.fileName = FileUpload1.FileName;

    PageAsyncTask pageAsyncTask = new PageAsyncTask(BeginAsync, EndAsync, null, fwd, false);
    RegisterAsyncTask(pageAsyncTask);
}

private IAsyncResult BeginAsync(object sender, EventArgs e, AsyncCallback cb, object extraData)
{
    FileWriteData fwd = (FileWriteData)extraData;
    FileStream fs =
    new FileStream(Server.MapPath(@"images\" + fwd.fileName),  
      FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write,  
      64 * 1024, true);
    fs.SetLength(fwd.bufferLength);
    fwd.fs = fs;
    IAsyncResult asyncResult = fs.BeginWrite(fwd.fileBytesBuffer, 0,  
      fwd.bufferLength, cb, fwd);
    return asyncResult;
}

private void EndAsync(IAsyncResult asyncResult)
{
    object extraData = asyncResult.AsyncState;
    FileWriteData fwd = (FileWriteData)extraData;
    using (FileStream fs = fwd.fs)
    {
      fs.EndWrite(asyncResult);
      fs.Close();
    }
    Label1.Text = "File saved.";
}

读取文件遵循相似的模式,使用Stream方法BeginRead和EndRead。

注意

对异步读写文件要小心,因为异步I/O会有额外的开销,例如线程切换。使用异步I/O操作小文件可能使网站更慢。找出通常上传至网站的文件大小,然后使用典型文件大小进行测试。

异步文件读写并不总是异步执行的。使用压缩文件系统读写和写压缩文件总是同步执行的。扩展写文件也是同步的,但是可以使用FileStream.SetLength给出文件的最终大小解决这个问题。查看一个任务是同步还是异步执行的,检查EndAsync中的IAsyncResult.CompletedSynchronously标志位。

异步web请求

网站在服务器端使用代码加载另一个网页是很少见的,所以这里没有给出例子。通过缓存页面或者使用单独的程序定时读取页面存储到数据库,而不是在每个请求中读取页面,可以极大地提高性能。

如果仍然希望在处理请求时加载其它web页面,在http://msdn.microsoft.com/en-us/library/system.net.webrequest.begingetresponse.aspx可以找到例子。

修改配置

如果服务器上很多空闲的CPU和内存,可以调高执行请求的线程的最大数量。

运行时管理执行请求的线程的数量。所以,即使增加线程的最大数量,也一定会有更大的线程可用。

警告:小心配置。有可能花了很多时间进行配置,结果反而有害。在加入生产环境前,进行测试。

IIS6,IIS 7经典模式

在IIS6和使用经典模式的IIS 7中,线程最大数量由以下公式决定:

最大线程数量 = (最大工作线程 * CPU数量) - 最小空闲线程

CPU数量指服务器处理器核的数量。如果是双核CPU,可以看作两个CPU。

最大工作线程是在machine.config中的processModel元素的一个属性:

<processModel maxWorkerThreads="20" .... >

最小空闲线程是在machine.config中的httpRuntime元素的一个属性:

<httpRuntime minFreeThreads="8"  .... >

I/O相关配置

如果调用很多web service或访问很多其它网站,下列设置可能有用:

  • maxIoThreads:线程池中每个CPU用于I/O处理的线程的最大数量。它必须大于或等于最小空闲线程。
    <processModel maxIoThreads="20" ... >
  • maxconnection:设置线程池中连接同一IP地址的最大线程数量。例如,下面的设置允许对http://www.example.com有4个连接,对其它地址有两个连接。
    <configuration>
      <system.net>
        <connectionManagement>
          <add address = "http://www.example.com" maxconnection = "4" />
          <add address = "*" maxconnection = "2" />
        </connectionManagement>
      </system.net>
    </configuration>

ASP.NET 2.0

在.NET1.1时期,Microsoft总结出上一节中的默认配置对于大多数网站是太低了,因为这些设置使得线程最大值小于服务器硬件能够处理的数量。所以,在ASP.NET 2.0中,引入了autoConfig属性:

<processModel autoConfig="true"/>

当值为true时,autoConfig在运行时修改配置如下:

  • 设置maxWorkerThreaders和maxIoThreads为100
  • 设置maxconnection 为12*CPU数量
  • 设置minFreeThreads为88*CPU数量
  • 设置minWorkerThreads为50

每个CPU同时执行的线程数为12,这对于大多数网站是足够的。然而,如果网站有很多同步代码等候数据库、web services等,可以设置autoConfig为false,然后自己修改。

IIS 7集成模式

当使用集成模式时,同时执行的请求的最大数量就不是由上面几节描述的配置决定的,而是由下面的设置定义的:

  • maxConcurrentRequestsPerCPU:限制每个CPU执行的请求的数量,即使有更多的线程可用。在.NET 3.5及以前的版本中,默认值是12,在.NET 4中是5000。如果设为0,没有限制。
  • maxConcurrentThreadsPerCPU:限制每个CPU处理请求的线程的数量。默认值是0,没有限制。

这两个值必须一个为0,另一个不为0。

.NET 3.5 SP1以前的版本,如果需要修改这些值,必须在注册表的HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ASP.NET\2.0.50727.0位置加入同名DWORD键。

.NET 3.5 SP1以前的版本,可以在C:\Windows\Microsoft.NET\Framework\v2.0.50727或C:\Windows\Microsoft.NET\Framework\v4.0.30128的aspnet.config中加入这些设置:

<system.web>
  <applicationPool maxConcurrentRequestsPerCPU="12"  
    maxConcurrentThreadsPerCPU="0" requestQueueLimit="5000"/>
</system.web>

在.NET4中,maxConcurrentRequestPerCPU的默认值是5000,这意味着实际没有做任何限制。

当maxConcurrentRequestPerCPU设为5000并且maxConcurrentThreadsPerCPU设置为0时,处理请求的最大线程数量实际时线程池中的线程数量。从ASP.NET 2.0起,限制是每CPU100线程,所以,在双核CPU机器上,就是200线程。

如果要提高这个限制,使用System.Threading命名空间的ThreadPool对象的SetMaxThreads属性进行设置。

警告:小心设置,更多的线程意味着更多的任务切换开销。

最大队列大小

当请求进入速度比处理速度快时,就会被排队。如果队列太长,ASP.NET返回给新请求“503—Server Too Busy”错误。

最大队列大小是通过machine.config中的ProcessModelSection元素的requestQueueLimit属性设置:

<processModel requestQueueLimit ="5000" … >

超时

如果页面同步访问外部资源并且外部资源非常慢,这些页面就可能超时。如果没有超时机制,页面就可能永久阻塞线程。

.NET 2.0及更高的默认超时时间是110秒,.NET 1.0和1.1是90秒。在一个繁忙的网站,阻塞线程110秒时间太长了。

设置超时时间为30秒,可能会导致更多的超时,但会更好的利用线程。

超时在web.config中的httpRuntime元素的executionTime属性设置:

<system.web>
  <httpRuntime executionTimeout="30" />
</system.web>

也可以单独设置在每个页面的超时时间:

<configuration>
  ...
  <location path="Page.aspx">
    <system.web>
      <httpRuntime executionTimeout="30" />
    </system.web>
  </location>
  ...
</configuration>

另外一个选择时异步访问外部资源。

更多资源

posted on 2010-11-30 17:25  9527  阅读(1287)  评论(0编辑  收藏  举报