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.