Da async/await pattern blev introduceret med C# 5, blev hele .NET frameworket gennemarbejdet, så relevante asynkrone metoder blev awaitable og fik Task som returværdi.

Det gælder ikke Windows Phone API’en.  Specielt gælder det ikke for WebClient, der i .NET 4.5 bl.a. har fået DownloadDataTaskAsync, der returnerer en Task, og dermed er awaitable.  Til Silverlight må man nøjes med DownloadDataAsync, som er baseret på Event-Based Asynchronous Pattern (EAP) gennem DownloadDataCompleted.

I System.Threading.Tasks namespacet findes en klasse kaldet TaskCompletionSource.  Denne klasse er netop beregnet til EAP scenarier, fordi den opretter en task, som man kan sætte op til at vente på, at en event bliver kaldt.  Dette gøres vha. af SetResult og SetException.  Kalder man en af de to metoder, vil den tilhørende Task afsluttes.  Vha. af TaskCompletionSource har jeg lavet følgende awaitable extension metoder til WebClient, som gør det muligt at lave awaitables for hver af de 4 HTTP metoder GET, POST, PUT og DELETE.  Metoderne benytter JSON via Json.NET:

    public static class WebClientExtensions
    {
        public static Task<T> GetAsync<T>(this WebClient client, Uri uri)
        {
            var tcs = new TaskCompletionSource<T>();
            client.DownloadStringCompleted += (s, e) =>
                {
                    if (e.Error == null)
                    {
                        T result = JsonConvert.DeserializeObject<T>(e.Result);
                        tcs.SetResult(result);
                    }
                    else
                    {
                        tcs.SetException(e.Error);
                    }
                };
            client.DownloadStringAsync(uri);
            return tcs.Task;
        }

        public static Task PostAsync<T>(this WebClient client, Uri uri, T item)
        {
            var tcs = new TaskCompletionSource<string>();
            client.Headers["Content-Type"] = "application/json";
            client.UploadStringCompleted += (s, e) =>
            {
                if (e.Error == null)
                {
                    tcs.SetResult(e.Result);
                }
                else
                {
                    tcs.SetException(e.Error);
                }
            };

            string data = JsonConvert.SerializeObject(item);

            client.UploadStringAsync(uri, "POST", data);
            return tcs.Task;
        }

        public static Task PutAsync<T>(this WebClient client, Uri uri, T item)
        {
            var tcs = new TaskCompletionSource<string>();
            client.Headers["Content-Type"] = "application/json";
            client.UploadStringCompleted += (s, e) =>
            {
                if (e.Error == null)
                {
                    tcs.SetResult(e.Result);
                }
                else
                {
                    tcs.SetException(e.Error);
                }
            };

            string data = JsonConvert.SerializeObject(item);

            client.UploadStringAsync(uri, "PUT", data);
            return tcs.Task;
        }

        public static Task DeleteAsync(this WebClient client, Uri uri)
        {
            var tcs = new TaskCompletionSource<string>();
            client.UploadStringCompleted += (s, e) =>
            {
                if (e.Error == null)
                {
                    tcs.SetResult(e.Result);
                }
                else
                {
                    tcs.SetException(e.Error);
                }
            };

            client.UploadStringAsync(uri, "DELETE", "");
            return tcs.Task;
        }

Du kan læse mere om TaskCompletionSource hos Stephen Toub, som er en af de ypperste, når det kommer til parallelisering i .NET.

Jeg har haft stor glæde af de ovenstående WebClient extensions, men min glæde var endnu større, da jeg fornylig læste, at Microsoft arbejder på en port af HttpClient til Windows Phone, og har gjort det tilgængeligt via NuGet.  Jeg har endnu ikke prøvet det, men det virker lovende.

Kigger man i eksempler på nettet på brug af Task, ser man ofte Thread.Sleep anvendt inden i den pågældende Tasks action:

Task.Factory.StartNew(() =>
{
    // Lad være med det her
    Thread.Sleep(1000);
});

Det er sjældent, man bruger Thread.Sleep i andet end eksempelkode, men skulle det ske, må man ikke gøre det sammen med en Task som ovenfor.  Der er ikke garanteret nogen sammenhæng mellem én tråd og en Task.  Task kan skifte mellem tråde – ja, i nogle tilfælde vil en Task endda eksekvere på samme tråd som kalderen, alt efter hvordan man bruger Task.Factory.StartNew og Task.Run.

Hvis man rent faktisk ønsker at stoppe en Task i en tidsperiode, skal man i stedet for Thread.Sleep bruge Task.Delay.

Benytter man HTTP stacken fra Silverlight – herunder kald af WCF services over HTTP – sker kaldet asynkront.  Derfor skal man passe på, hvis man opdaterer brugerkontroller i sin callback funktion, da den kan blive kaldt på en anden tråd end den, der ejer brugerkontrollen.  Det er gamle nyheder.  Bruger man et pattern som f.eks. MVVM, hvor opdatering af ens view sker gennem binding til properties på en viewmodel, sker marshalling til UI tråden automatisk af Silverlight’s binding mekanisme.  I andre tilfælde er man nødt til selv at sørge for, at brugerinterfacet opdateres på den rigtige tråd f.eks. gennem Dispatcher.

Jeg har først fornylig opdaget, at callbacks fra WCF services i nogle tilfælde automatisk bliver flyttet til den kaldende tråd i Silverlight.  Det gælder også Silverlight til WP7.  Det sker i det tilfælde, hvor man benytter client proxy klasserne, der genereres, når man vælger “Add service reference” fra Visual Studio. 

Lad os sige, at jeg har en service kaldet Service1 med en metode DoWork.  Jeg har tilføjet en reference til servicen gennem VS, hvorved et namespace ServiceReference1 med en proxy klasse kaldet Service1Client er blevet oprettet.  Følgende kode er dermed fuldt lovlig:

label1.Text = Thread.CurrentThread.ManagedThreadId.ToString();

ServiceReference1.Service1Client service = new ServiceReference1.Service1Client();
service.DoWorkCompleted += (s, args) =>
    {
        // Dette kald er fuldt lovligt, og label1 og label2 vil vise samme thread id.
        label2.Text = Thread.CurrentThread.ManagedThreadId.ToString();
    };
service.DoWorkAsync();

Kigger man dybt nok i de genererede klasser, kan man se, at de klassen AsyncOperation benyttes til at flytte kaldet tilbage til den kaldende tråd.  Har man brug for at lave noget arbejde i sin callback fra sin service, må man undlade at bruge den genererede client proxy og f.eks. bruge ChannelFactory eller noget andet.  Forrige eksempel er funktionelt ækvivalent med nedenstående, hvor marshalling til UI tråden sker manuelt gennem brug af AsyncOperation.  Bemærk at AsyncOperation skal oprettes gennem AsyncOperationManager.CreateOperation:

AsyncOperation asyncOp;

void CallWcf()
{
    asyncOp = AsyncOperationManager.CreateOperation(1);
    ServiceReference1.IService1Channel channel 
        = new ChannelFactory<ServiceReference1.IService1Channel>("BasicHttpBinding_IService1")
        .CreateChannel();
    channel.BeginDoWork(DoWorkCallback, channel);
}

void DoWorkCallback(IAsyncResult result)
{
    ServiceReference1.IService1Channel channel = result.AsyncState as ServiceReference1.IService1Channel;
    channel.EndDoWork(result);

    asyncOp.PostOperationCompleted(new SendOrPostCallback(o =>
        {
            label2.Text = (string) o;
        }), Thread.CurrentThread.ManagedThreadId.ToString());
}

I stedet for AsyncOperation kunne man også vælge at bruge SynchronizationContext eller Dispatcher direkte.  Faktisk er AsyncOperation blot en wrapper omkring SynchronizationContext, og i Silverlight er SynchronizationContext en instans af DispatcherSynchronizationContext, som igen er en wrapper omkring Dispatcher.  Så Dispatcher er den rigtige helt her.

De fleste som har programmeret Windows applikationer med et brugerinterface ved, at man ikke kan tilgå vinduer fra andre tråde end den tråd, der har oprettet vinduet, og som står for at pumpe beskedkøen til vinduet.

I klassisk Win32 er det ganske simpelt at overholde denne regel, da man med PostMessage nemt kan placere en besked i et vindues beskedkø, selvom man kalder fra en anden tråd.  PostMessage returnerer øjeblikkeligt.  SendMessage er en lidt anden historie.  Såvidt jeg husker, bør man ikke kalde den fra en anden tråd, da man kan risikere, at man bliver blokeret i længere tid indtil tråden, der ejer vinduet, vågner op og får pumpet nogle beskeder.  Hvis SendMessage kaldes fra den ejende tråd, bliver Windows proceduren kaldt direkte, hvilket effektivt svarer til at placere en besked forrest i beskedkøen og pumpe med det samme.

I .NET og WinForms benyttes Control.InvokeRequired, Control.Invoke og Control.BeginInvoke.

Jeg har altid tænkt på Control.Invoke som svarende til SendMessage og Control.BeginInvoke som svarende til PostMessage.  I praksis passer det også meget godt, men det er ikke helt det samme der sker.  Faktisk bliver PostMessage kaldt af både Invoke og BeginInvoke.

Når man benytter Invoke og BeginInvoke, registrerer WinForms en ny besked i Windows og benytter PostMessage til at anbringe beskeden i beskedkøen for vinduet (i Win32 er der i princippet ingen forskel på kontroller og vinduer – alt er vinduer).  Forskellen på Invoke og BeginInvoke er, at Invoke venter indtil vinduets beskedkø har behandlet beskeden, mens BeginInvoke returnerer med det samme.

Resultatet er, at der for Invoke godt kan gå længere tid, inden funktionen returnerer, hvis der allerede ligger en milliard beskeder i køen, eller vinduet af den ene eller anden grund ikke lige pumper beskedkøen. 

Medmindre man virkelig har brug for at vente, vil BeginInvoke derfor være at foretrække.

Følgende stump kode er den anbefalede måde at bruge monitors på, og det er identisk med den kode, som C#'s lock statement genererer.

Monitor.Enter(lockobject);
try
{
    //...
}
finally
{
    Monitor.Exit(lockobject);
}

 

Der er faktisk en potentiel bug i denne kode (iflg. Joe Duffy).  Kompileren er i sin fulde ret til at indsætte instruktioner mellem Monitor.Enter og try.  Et forsøg viser, at C# kompileren ganske rigtigt indsætter en NOP instruktion i debug build. 

Det betyder, at man (hvis man er meget uheldig) kan opleve asynkrone thread exceptions for netop denne instruktion.  Det kan f.eks. ske, hvis en anden tråd kalder Thread.Abort.  Sker det, vil finally blokken aldrig eksekvere, og vi har en såkaldt abandoned monitor.  Det er nok de færreste, for hvilke det er et reelt problem, men det kan ske.

Det viser sig, at C# teamet har taget højde for dette.  I release builds vil C# kompileren ikke indsætte ekstra instruktioner efter Monitor.Enter, og Monitor.Enter er lavet på en speciel måde, så næste instruktion i vores kode ovenfor vil foregå inde i try blokken.

Joe Duffys bog fik mig til at tænke på connections.  Hvis DbConnection.Open står uden for try blokken, kan man i sjældne tilfælde risikere, at finally blokken indeholdende DbConnection.Close ikke bliver kaldt pga. en exception.  Heldigvis er det et krav til DbConnection.Close, at man skal kunne kalde den flere gange uden at få en exception i hovedet uanset om ens connection er åben eller ej.  Derfor kan man med sindsro flytte DbConnection.Open indenfor try blokken.