Multi-core processorRazvojni tim je dobio zahtev za doradu aplikacije koja automatizuje zaprimanje robe i štampu deklaracija u magacinu. Posle inicijalnih podešavanja, aplikacija dobija ulaz sa bar kod čitača, a kao izlaz, osim što u bazi označava da je artikal zaprimljen, šalje deklaraciju artikla na štampu. Ideja je da po jedan bar kod skener i štampač budu vezani na jedan računar. Inicijalno je projekat bio složeniji, ali je promenom poslovnih procesa, ostala potreba samo za ovim delom funkcionalnosti. Uzeli smo parče koda sa serverskog dela, c# .net web servisa, koji je komunicirao sa bazom i štampačima, dodali u klijentsku Winforms aplikaciju kao biblioteku i to je proradilo.

Dobili smo desktop aplikaciju koja je imala svu logiku na klijentu, instaliranje softvera smo sveli na copy/paste i instalaciju drajvera za štampače, sve je idealno. Postavili smo test verziju, i dobili odgovor od korisnika da aplikacija funkcioniše ali je spora i koči. Gde je greška? Pri migraciji koda za štampu sa servisa na lokalni računar pozivi ka štampaču su postali sinhroni, što znači da aplikacija nije prihvatala ulaz sa bar kod čitača sve dok traje štampa. U praksi, ako imate četiri artikla jedan ispod drugog i pročitate sva četiri bar koda u nekoliko sekundi štampač će za to vreme stići da izbaci jednu ili dve deklaracije i samo će ti artikli biti zaprimljeni. Pored samih asinhronih poziva potrebno je vratiti i prikazati rezultat na korisničkom interfejsu.

Da pojednostavimo

Imamo dve metode: spora metoda work, puno posla simuliramo zaustavljanjem niti na 3 sekunde i metodu print, koja prikazuje neke vrednosti u labeli na formi. Mi želimo da naša aplikacija ima mogućnost da pozove metodu work bez obzira da li su se prethodni pozivi metodi izvršili, a da po završetku svakog poziva prikaže rezultat izvršenja na ekranu koristeći metodu print.

[csharp]
private string work()
{
Thread.Sleep(3000);
return Thread.CurrentThread.ManagedThreadId.ToString();
}

private void print(string prefix, string value)
{
label1.Text += string.Format("{0}: {1}\n", prefix, value);
}
[/csharp]

Za početak da stavimo dugme na našu kontrolu, u događaju OnClick dodamo poziv našim metodi work i odštampamo rezultat, može u jednoj liniji ovako:

[csharp]
private void btnSync_Click(object sender, EventArgs e)
{
print("sync", work(chkRaiseException.Checked));
}
[/csharp]

Ovo je klasičan sinhroni poziv, što znači da će na klik na naše Sync dugme da se odradi poziv metodi work, gde ćemo zabaviti naš računar na 3 sekunde i u međuvremenu nećemo imati nikakav odziv od naše aplikacije, nećemo moći ni da šetamo prozor po radnoj površini, jer se cela aplikacija izvršava u jednoj niti, sinhrono. Kada se proces završi, možemo da ga pokrenemo ponovo.

Nama treba rešenje koje će održati responzivnost korisničkog interfejsa, šta nudi .NET framework? BackgroundWorker izaziva laganu jezu. Druga varijanta je raditi direktno sa thread-ovima (nitima), što deluje komplikovano u poređenju sa našim problemom. Alternativa je Task Parallel Library za .NET 4.0 i Async CTP koji se reklamira kao deo novog framework-a 5.0 4.5, međutim dostupan je i za Visual Studio 2010, evo linka za download Visual Studio Async CTP (Version 3).

Task Parallel Library

Osnova biblioteke su taskovi koji predstavljaju asinhronu operaciju i imaju definisan tip rezultata operacije, koji može da bude i void. Task Parallel Library nam nudi TaskFactory.StartNew() metodu za kreiranje taskova, odnosno asinhrono kreiranje i izvršavanje delegata. Pošto sa StartNew zavrtimo task, potrebno je da sačekamo njegovo izvršenje i pokupimo rezultat, što možemo da uradimo metodom Task.ContinueWith(). Kako naša metoda može da izgleda ovako:

[csharp]
//create and start the Task
var someTask = Task.Factory.StartNew(() => work(chkRaiseException.Checked));
someTask.ContinueWith(
x => print("task", someTask.Result.ToString())
);
[/csharp]

Kako želimo da naš print piše u labelu sa forme, u trebaće nam i sinhronizacija sa UI thread-om:

[csharp]
//get UI thread context
var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();

//create and start the Task
var someTask = Task.Factory.StartNew(() => work(chkRaiseException.Checked));

someTask.ContinueWith(
x => print("task async", someTask.Result.ToString()),
uiScheduler
);
[/csharp]

U demou dugme Async Task pokreće praktično ovaj kod, sa dodatnom opcijom da čekiranjem opcije Raise Exception možete da izazovete exception u metodi work. Ovaj kod radi ono što očekujemo i to asinhrono, dok se pokrenuti procesi izvršavaju u pozadini, možete da šetate prozor aplikacije bez ikakvih problema i pokrećete nove procese, kako se koji proces završava prikazuju se rezultati na korisničkom interfejsu. Startujte projekat bez debug-a (Ctrl + F5) i probajte malo, iznenađenje? Ako čekirate Raise Exception i startujete Async Task, aplikacije će jednostavno progutati exception, korisnik nije obavešten da je došlo do problema. Ako se malo duže poigrate i startujete više taskova, sa isključenom i uključenom opcijom Raise Exception, stiže još veće iznenađenje, aplikacija pada AsyncWinForms has stopped working, kao što možete da vidite na slici.

AsyncWinForms has stopped working

Potrebno je obraditi izuzetke iz asinhronog poziva, preporučen način je obrada događaja TaskScheduler.UnobservedTaskException. Evo koda koji dodajemo pre samo asinhronog poziva (dugme Async Task Ex)

[csharp]
TaskScheduler.UnobservedTaskException += (s, args) =>
{
foreach (var ex in args.Exception.Flatten().InnerExceptions)
MessageBox.Show(ex.Message);
args.SetObserved();
};
[/csharp]

Posle obrade izuzetaka ovo postaje korektno rešenje koje može da se iskoristi za probleme ovog tipa.

Async / Await

Biblioteka AsyncLibrary nam omogućava da isti problem rešimo elegantnije, funkcionalnost je slična, međutim dobili smo nove ključne reči async i await koje nam omogućavaju jednostavniju sintaksu. Async i await koristimo u paru, await kaže kompajleru gde da iseče metodu, odnosno da prebaci dalje izvršavanje u novu nit i da se vrati po završetku operacije, pored toga omogućava da tretiramo rezultat tipa Task<T> kao da je samo T. U našem slušaju koristimo rezultat tipa Task<string> kao običan string. Async obaveštava kompajler da u metodi postoji await, mi je koristimo na metodama koje obrađuju OnClick događaj naših dugmića. Async i await sami po sebi ne omogućavaju paralelno izvršavanje koda, kod ispred koga stoji await treba da obezbedi pokretanje background thread-a, kreiranjem Task-a ili pozivom neke asinhrone metode.

[csharp]
private async void btnASync_Click(object sender, EventArgs e)
{
var someTask = Task.Factory.StartNew(
() => work(chkRaiseException.Checked)
);
await someTask;
print("async", someTask.Result.ToString());
}
[/csharp]

Ako samo kreiranje taska izmestimo u metodu work, odnosno neka to bude nova metoda workTask:

[csharp]
private Task workTask(bool raiseException)
{
return Task.Factory.StartNew(() => work(raiseException));
}
[/csharp]

Onda naš asinhroni poziv može da bude u jednoj liniji:

[csharp]
private async void btnAsyncAwait_Click(object sender, EventArgs e)
{
print("async", await workTask(chkRaiseException.Checked));
}
[/csharp]

Zaključak

Task Parallel Library i Async omogućavaju da u nekoliko linija koda napravite asinhroni poziv i/ili paralelizujete izvršavanje .NET koda. Potrebno je da razumete šta se dešava ispod haube da bi bili svesni da postoje zamke koje nisu očigledne, dobra referenca je msdn stranica Potential Pitfalls in Data and Task Parallelism, mada se tamo ne pominje problem na koji smo mi naišli sa neobrađenim izuzecima. Mada ima i kritika, kao što je ova CodeProject, sigurno je da će Visual Studio 2012 doneti veliki napredak u asinhronom i paralelnom programiranju, ako ste propustili release candidat je dostupan za download.

Resursi

Task Parallel Library, Async CTP, Potential Pitfalls in Data and Task Parallelism, MSDN

Crashes, Task Parallel Library (TPL) and the UnobservedTaskException, Benjamin Bondi

Source kod demo projekta C# , da bi se demo kompajlirao neophodan je Visual Studio 2010 SP1 i instaliran Visual Studio Async CTP (Version 3)

2 thoughts on “Asinhroni C# (async / await)

  1. Dimitrije says:

    Odličan blog, ja se nažalost iz legacy razloga bakćem sa VB6. Nema prigodnih biblioteka, moramo da se snalazimo sa ActiveX EXEima.

    1. Branimir says:

      Hvala Dimitrije. Može ActiveX iz .NET-a? Kao priprema za migraciju 🙂

Leave a Reply