Julekalender 2004 om Whidbey


22. dec 2004 00:10

Afhænger en eller flere Web Forms af længerevarende processering så kan den givne web applikation hurtigt få "åndenød" - især hvis der skulle forvilde sig flere brugere ind på sitet samtidig :-). Som udgangspunkt er ASP.NET synkron afvikling af de enkelte Web Forms, og der er en (forholdsvis lav) grænse for hvor mange samtidige tråde ASP.NET starter op - dette begrænser antallet af samtidige brugere.

Web Services kaldt fra en Web Form

Som eksempel så lad os antage at en Web Form har brug for at kalde en WebService. Denne WebService er krævende og tager nogle sekunder. I disse sekunder er der på webserveren låst en tråd til Web Form'en. Ved mange samtidige brugere af websitet vil dette sløve utroligt meget ned på performance.

I ASP.NET 1.1 kan man registrere en PreRequestHandlerExecuteAsync i Globals.asax, man kan så afvikle det asynkrone kald inden selve Web Formen opbygges og derved undgå at blokere nogle request tråde. Det fungerer, men det er uhyre bøvlet at kode.

I ASP.NET 2.0 er det bygget asynkron afvikling direkte ind på Web Formen. Vi har således den normale programmeringsmodel med adgang til samtlige kontroller på siden både for den del af siden der afvikles synkront og den der afvikles asynkront.

Er det besværet værd?

Web Formen herunder indeholder 2 tekstfelter, en knap samt 2 labels. Når knappen trykkes så kaldes en webservices, der kan vende teksten, 2 gange - en for hver tekst. Denne webservice har indbygget en 2 sekunders forsinkelse.

I en ASP.NET 1.1 implementation, hvor 3 forskellige scenarier var implementeret:

  1. Synkrone kald til de 2 webservices (således at et request teoretisk tager 4 sekunder)
  2. Asynkrone kald til de 2 webservices (således at et request teoretisk tager 2 sekunder)
  3. Ved brug af PreRequestHandlerExexuteAsync (også 2 sekunder i teorien, men uden at låse en request tråd i disse 2 sekunder

Disse 3 implementationer blev udsat for en stress test, der simulerede 100 samtidige brugere - testen kørte i 60 sekunder. Antallet af gennemførte requests var:

  1. 160 stk (gennemsnitlig requesttid 35 sekunder)
  2. 346 stk (gennemsnitlig requesttid 17 sekunder)
  3. 1867 stk (gennemsnitlig requesttid 3 sekunder!)

Resultatet er godt nok på ASP.NET 1.1, men resultatet for en ASP.NET 2.0 implementation vil nok ikke afvige meget fra dette.

Resultatet taler sit tydelige sprog - hvis du bekymrer dig om throughput i din Web Forms applikation og den benytter sig af anden længerevarende processesering så er det tid for "Asynkron afvikling af ASP.NET 2.0 sider"!

Et eksempel

Her er implementationen af ovenstående eksempel i ASP.NET 2.0. For at gøre en side klar til asynkron afvikling sætter vi async="true" i page direktivet på formen:

<%@ Page Language="VB" AutoEventWireup="false" Async="true" ... %>

I koden skal vi registrerer et par metoder: BeginAsynData og et EndAsyncData metoder for at siden bliver afviklet asynkront. Alle de "normale" AsP.NET events afvikles på request tråden frem til PreRender eventet. Herefter kaldes den registrerede BeginAsyncData metode hvorefter request tråden frigives. Når EndAsyncData kaldes så tildeles siden igen en request tråd og selve renderingen og de sidste events afvikles.

Den asynkrone del af siden skal kun afvikles hvis knappen trykkes, derfor registreres den asynkrone del i Button1_Click. Selve afviklingen af de asynkrone webservices igangsættes i BeginGetAsyncData Da der er brug for 2 asynkrone webservice request, er der lavet en hjælpe klasse (CombineMultipleAsyncResults), der kan forene de 2 asynkrone resultat objekter til et asynkront resultat objekt.

Når begge de 2 asynkrone webservices afsluttes kaldes EndGetAsyncData her aflæses resultatet fra de 2 webservices

Partial Class OrdVender03TekstViaForm_aspx

  Dim _proxy As New WebServices.OrdvenderWebService

  Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs)

    Dim bh As BeginEventHandler = AddressOf BeginGetAsyncData
    Dim eh As EndEventHandler = AddressOf EndGetAsyncData

    AddOnPreRenderCompleteAsync(bh, eh)

  End Sub

  Private Function BeginGetAsyncData(ByVal src As Object, _
                                     ByVal args As System.EventArgs, _
                                     ByVal cb As AsyncCallback, ByVal state As Object) _
                                     As System.IAsyncResult

    Dim async(1) As System.IAsyncResult
    Dim asyncBoth As New CombineMultipleAsyncResults(Nothing, cb, async)

    async(0) = _proxy.BeginLangsomOrdVender( _
                 txtInput1.Text, AddressOf asyncBoth.CheckIfCompleted, Nothing)
    async(1) = _proxy.BeginLangsomOrdVender( _
                 txtInput2.Text, AddressOf asyncBoth.CheckIfCompleted, Nothing)

    Return asyncBoth

  End Function

  Private Sub EndGetAsyncData(ByVal ar As IAsyncResult)

    lblResultat1.Text = _
      _proxy.EndLangsomOrdVender(CType(ar, CombineMultipleAsyncResults).Results(0))
    lblResultat2.Text = _
      _proxy.EndLangsomOrdVender(CType(ar, CombineMultipleAsyncResults).Results(1))

  End Sub

End Class


Public Class CombineMultipleAsyncResults
  Implements System.IAsyncResult

  Private _state As Object
  Private _results() As System.IAsyncResult
  Private _callback As AsyncCallback

  Public Property Results() As System.IAsyncResult()
    Get
      Return _results
    End Get
    Private Set(ByVal value As System.IAsyncResult())
      _results = value
    End Set
  End Property

  Public Sub New(ByVal state As Object, ByVal callback As AsyncCallback, _
                 ByVal ParamArray results() As System.IAsyncResult)
    _state = state
    _callback = callback
    _results = results
  End Sub


  Public ReadOnly Property AsyncState() As Object _
      Implements System.IAsyncResult.AsyncState
    Get
      Return _state
    End Get
  End Property

  Public ReadOnly Property AsyncWaitHandle() As System.Threading.WaitHandle _
      Implements System.IAsyncResult.AsyncWaitHandle
    Get
      Return _results(0).AsyncWaitHandle
    End Get
  End Property

  Public ReadOnly Property CompletedSynchronously() As Boolean _
      Implements System.IAsyncResult.CompletedSynchronously
    Get
      For Each res As System.IAsyncResult In _results
        If Not res.CompletedSynchronously Then
          Return False
        End If
      Next
      Return True
    End Get
  End Property

  Public ReadOnly Property IsCompleted() As Boolean _
      Implements System.IAsyncResult.IsCompleted
    Get
      For Each res As System.IAsyncResult In _results
        If Not res.IsCompleted Then
          Return False
        End If
      Next
      Return True
    End Get
  End Property

  Public Sub CheckIfCompleted(ByVal asyncRes As System.IAsyncResult)

    If (IsCompleted()) Then
      _callback.Invoke(Me)
    End If
  End Sub

End Class


Abonnér på mit RSS feed.   Læs også de øvrige indlæg i denne Blog.