Yllätyin hieman, kun koodailin erästä Silverlight 3 -projektia, joka käyttää asynkronisia lukuoperaatioita HTTP-yhteyksissä. Luettuaan melko suuren määrän dataa verkosta ohjelma sai yhtäkkiä Stack Overflow -virheilmoituksen, vaikkei sille ollut mitään järjellistä selitystä.

Pienellä googlailulla kävi ilmi, että .NETin asynkroniset operaatiot eivät olekaan aina asynkronisia. Joskus Stream.BeginRead() suorittaa lukuoperaation välittömästi synkronisesti, jolloin callback-metodia kutsutaan jo ennen kuin BeginRead() palautuu.

.NETin "luonnollinen" tapa suorittaa asynkronisia operaatioita on tällainen:

Stream stream;
AsyncCallback callback = new AsyncCallback(ReadComplete);

void StartReading()
{
  stream.BeginRead(callback, null);
}

void ReadComplete(IAsyncResult result)
{
  int nBytes = stream.EndRead(result);
  stream.BeginRead(callback, null);
}

Hetken pohtimalla koodista huomaa, että jos BeginRead() kutsuu aina ReadComplete():a ennen palautumistaan, niin tämä johtaa jatkuvaan rekursioon ja ajan myötä aiheuttaa stack overflown.

Oikea tapa on tarkastaa IAsyncResult.CompletedSynchronously -muuttujasta, oliko operaatio todellisuudessa synkroninen. Tällöin voidaan välttää rekursio esimerkiksi näin:

Stream stream;
AsyncCallback callback = new AsyncCallback(ReadComplete);

void StartReading()
{
  IAsyncResult result = stream.BeginRead(callback, null);
  if (result.CompletedSynchronously) ReadCompleteAsync(result);
}

void ReadComplete(IAsyncResult result)
{
  if (!result.CompletedSynchronously) ReadCompleteAsync(result);
}

void ReadCompleteAsync(IAsyncResult result)
{
  while (true) {
    int nBytes = stream.EndRead(result);
    result = stream.BeginRead(callback, null);
    if (!result.CompletedSynchronously) return;
  }
}

Ylläolevassa esimerkissä synkroniset operaatiot ajetaan peräkkäin yhden while (true) -luupin sisällä, joten rekursio vältetään.

PS. Tämä koodi on vain esimerkki, sitä ei ole testattu.

Published 9.5.2009