2. Event-uri si thread-safety
public class Class1
{
public event EventHandler MyEvent;
protected void RaiseMyEvent(EventArgs e)
{
var handler = MyEvent;
//multicast delegate-ul este imutabil. copierea este safe, desi poate fi out-of-date
//din cauza faptului ca storage-ul pentru event nu este volatile
if (handler != null) handler(this, e);
}
protected void RaiseMyEventBad(EventArgs e)
{
if (MyEvent != null)
//daca un alt thread dezataseaza ultimul eventhandler aici, MyEvent devine null
MyEvent(this, e);
}
}
3. Event-uri si thread-safety
Modificarea unui event din alt thread este thread-safe. Compilator-ul
emite metode cu atributul synchronized desi echipa CLR nu recomanda
folosirea: lock(this) si lock(typeof(T))
4. Event-uri si thread-safety
lock(this) si lock(typeof(T)) nu sunt safe deoarece nu controlam in mod
exclusiv obiectul pe care aplicam lock.
System.Type poate fi partajat intre mai multe AppDomain, si executia unui
lock(typeof(T)) in alt Application domain poate interfera cu executia
codului din Application Domain-ul curent.
5. Event-uri si thread-safety
In .NET 4.0 compilatorul genereaza cod thread-safe mai robust pentru
atasarea/dezatasarea evenimentelor folosind o tehnica lock free
6. Thread-safety and volatile
Compilatorul JIT optimizeaza agresiv in anumite configuratii de build. Din nefericire
optimizarea se face cu prezervarea intentiei codului doar din punct de vedere al unui singur
fir de executie.
private static void OptimizedAway()
{
// expresie constanta cunoscuta la compilare, are valoarea 0.
var value = (1 * 100) - (50 * 2);
// daca valoarea este 0 bucla nu se executa
for (Int32 x = 0; x < value; x++)
{
// acest cod nu e compilat doarece bucla nu se executa
Console.WriteLine("Jeff");
}
}
Pentru OptimizedAway se poate incerca inlining, cum metoda este vida, toate apelurile
metodei vor fie eliminate la compilare.
7. Thread-safety and volatile
Compilatorul JIT cu target x86 si optimize on, utilizeaza cea mai agresiva optimizare.
private static bool flag = false;
public static void Main(string[] args)
{
Console.WriteLine("Main: letting worker run for 5 seconds");
var t = new Thread(Worker);
t.Start();
Thread.Sleep(5000);
flag = true; //avem iluzia ca am semnalat corect oprirea catre worker thread
Console.WriteLine("Main: waiting for worker to stop");
t.Join();
Console.WriteLine("End. Press any key...");
Console.ReadKey();
}
private static void Worker(Object o)
{
Int32 x = 0;
while (!flag)
x++;
Console.WriteLine("Worker: stopped when x={0}", x);
}
Acest program nu se termina.
8. Thread-safety and volatile
Main creaza un thread si executa metoda Worker. Worker incrementeaza variabila i la infinit.
Main permite thread-ului sa ruleze 5 secunde pana cand semnalizeaza oprirea setand
campul boolean flag pe valoarea true. Worker thread trebuie sa afiseze valoarea la care a
ajuns variabila i, iar apoi thread-ul isi va termina executia. Main asteapta finalizarea thread-
ului folosind metoda Join.
Cand Worker este compilat, JIT vede faptul ca flag are valori discrete: true/false si nu se
schimba in interior-ul functiei Worker. Compilatorul va produce cod care verifica daca flag
este true, si daca da, afiseaza valoarea 0 si se termina. Daca flag este false atunci se
genereaza acest loop infinit. Se elimina prin optimizare citirea valorii flag pentru fiecare
iteratie.In acest fel bucla va rula teoretic mai rapid.
In aceasta situatie, Main seteaza inutil valoarea flag-ului = true, in metoda Worker nu mai
exista nici-o verificare.
Reproducerea aceastui comportament nu se realizeaza in debug mode, iar codul trebuie
compilat cu target x86 si optimize on.
Morala: corectitudinea unui program poate depinde de mai multi factori: versiunea de
compilator, switch-urile de compilare, target-ul, versiunea de JIT si chiar tipul de CPU.
Un alt exemplu interesant este tail recursion si JIT x64.
9. Thread-safety and volatile
Aplicatia anterioara va functiona corect in toate situatiile daca marcam campul flag ca si
volatile.
Volatile poate fi aplicat field-urilor statice sau ale instantelor de urmatoarele tipuri: Byte,
SByte, Int16, UInt16, Int32, UInt32, Char, Single, sau Boolean .
Se poate aplica si campurilor de tip referinta si oricarui tip enum atata vreme cat suportul
intern al campului enum este de tip Byte, SByte, Int16, UInt16, Int32, UInt32, Single, or
Boolean .
Compilatorul JIT ne asigura ca orice acces la campul marcat volatile se executa prin
Citiri volatile Thread.VolatileRead iar scrierile prin Thread.VolatileWrite.
De asemenea volatile indica compilatorului C# si JIT sa nu pastreze valoarea campului intr-un
registru de memorie si sa il citeasca de fiecare data din memorie.
CPU-urile de tip x86 x64 au cache coherency. Valoarea modificata de un thread care ruleaza
pe CPU1 este propagata pe CPU2 si este vizibila imediat. ( Citirea si scrierea sunt atomice
atat timp cat dimensiunea locatiei de memorie este 32 sau 64 biti in functie de arhitectura).
Procesoarele Itanium cu au comunicatie directa intre CPU-uri si prin urmare volatile poate
avea rolul de a face vizibile imediat modificarile dintr-un thread ruland pe CPU1 pentru un lat
thread ruland pe CPU2.
10. Locking
ReaderWriterLock vs ReaderWriterLockSlim vs lock
Un readerwriterlock permite mai multe thread-uri sa obtina access la citire
pentru o resursa si unui singur thread sa obtina acces de scriere la acea
resursa. Deasemenea, permite unui thread care detine read lock se poate
upgrada la writer fara sa elibereze read lock-ul detinut.
Gotcha: readerwriterlock este eficient atunci cand:
Exista mai multe thread-uri care citesc resursa decat thread-uri care scriu in
resursa. Daca sunt multe scrieri, resursa va fi oricum blocata in majoritatea
timpului iar cititorii vor trebui sa astepte.
Thread-urile care citesc resursa nu elibereaza foarte rapid read-lock-ul: daca
blocul se executa foarte rapid este mai performant folosirea lock decat rwlock.
11. Locking
ReaderWriterLock vs ReaderWriterLockSlim vs lock
Un readerwriterlock permite mai multe thread-uri sa obtina access la citire
pentru o resursa si unui singur thread sa obtina acces de scriere la acea
resursa. Deasemenea, permite unui thread care detine read lock se poate
upgrada la writer fara sa elibereze read lock-ul detinut.
Gotcha: readerwriterlock este eficient atunci cand:
Exista mai multe thread-uri care citesc resursa decat thread-uri care scriu in
resursa. Daca sunt multe scrieri, resursa va fi oricum blocata in majoritatea
timpului iar cititorii vor trebui sa astepte.
Thread-urile care citesc resursa nu elibereaza foarte rapid read-lock-ul: daca
blocul se executa foarte rapid este mai performant folosirea lock decat rwlock.
12. ThreadLocal<T> vs ThreadStatic
Campurile marcate cu ThreadStatic sunt initializate in constructorul static,
care se executa o singura data. Valoarea campului va fi initializata doar
pentru thread-ul sub care a fost rulat constructorul static.
ThreadStatic nu este recomandat inaplicatii ASP.NET si WCF. Exista un
moment in viata requestului in care infrastructura poate decide sa il
opreasca temporar si apoi sa il ruleze pe un alt thread. ( in cazul unei
incarcari mari). Pentru valorile care trebuie sa supravietuiasca doar pe
durata unui request se recomanda folosirea HttpContext.Current.Items iar
in cazul WCF: OperationContext.
Atributul ThreadStatic nu poate fi aplicat decat campurilor statice.
13. ThreadLocal<T> vs ThreadStatic
Un camp poate fi de tipul ThreadLocal<T> in ambele cazuri: static sau per
instanta.
Limitarile ThreadStatic (in contextul ASP.NET si WCF) se aplica si in cazul
ThreadLocal<T>
Constructorul poate primi un parametru factory de tip Func<T>, prin care
ne putem asigura ca valoarea este initializata corect.
Are proprietatile: IsValueCreated :bool si Value : T
implementeaza IDisposable.
Incepand cu .NET 4.5 exista posibilitatea sa obtinem lista completa de
valori pentru toate thread-urile unde membrul ThreadLocal a fost
initializat. Aceasta functionalitate este optionala, si este activata printr-un
parametru suplimentar din constructor: trackingEnabled: bool.
14. dynamic
Static typing – erorile sunt detectate la momentul compilarii, compilatorul genereraza cod
compact si eficient.
Dynamic typing – mai lent, poate fi convenabil in situatiile in care:
accesam structuri de date complexe care se mapeaza mai greu la o structura obiectuala
accesam obiecte COM care implementeaza IDispatch
accesam componente din alte limbaje care functioneza peste DLR: IronPython, IronRuby
dorim sa implementam multiple dispatch.
C#3.5
Object wordapp=new Word.Application(); //create Word object
Object fileName="MyDoc.docx"; //the specified Word document
Object argu= System.Reflection.Missing.Value;
Word.Document doc =
wordapp.Documents.Open(ref fileName, ref argu,ref argu, ref argu, ref argu, ref argu, ref argu, ref argu,ref
argu, ref argu, ref argu, ref argu, ref argu, ref argu, ref argu, ref argu);
C#4
dynamic wordapp = new Word.Application();
dynamic doc = wordapp.Documents.Open(FileName: "MyDoc.docx");
15. dynamic – exemplu 1
class DynamicXml : DynamicObject
{
public override bool TryGetMember(GetMemberBinder binder, out object result)
{ result = null;
var attr = this.xml.Attributes().FirstOrDefault(x => x.Name.LocalName == binder.Name);
if (attr == null) return false;
result = attr.Value; return true;
}
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{ result = null;
if (args.Length == 0) // intoarce un array de elemente
{ result = this.xml.Elements().Where(x=>x.Name.LocalName == binder.Name).Select(e => new
DynamicXml(e)).ToArray(); return true;
}
if (args.Length == 1 && args[0] is int) // intoarce elementul pentru parametrul index
{ result = new DynamicXml(this.xml.Elements().ToArray()[(Int32)args[0]]); return true; }
return false;
}
public override bool TryConvert(ConvertBinder binder, out object result)
{ result = null;
if (binder.Type != typeof(string)) return false;
result = this.xml.Value;
return true;
}
16. dynamic – exemplu 1
<?xml version="1.0" encoding="utf-8" ?>
dynamic xml = new DynamicXml(XmlFiles.Sample1); <systems>
<os>
var firstName = (string) xml.os(0).name(0); <name>Linux</name>
int count = xml.os().Length;
<author>community</author>
</os>
foreach(var os in xml.os())
{ <os>
foreach(var name in os.name()) <name>Windows</name>
{ <author>Microsoft</author>
Console.WriteLine((string)name); </os>
}
<os>
}
<name>MacOS</name>
<author>Apple</author>
Din nefericire dynamic nu poate fi folosit cu Linq. </os>
In versiunea 4 exista un hack folosind metode de </systems>
extensie, ce nu mai functioneaza in 4.5
18. ThreadPool
Exista un overhead legat de initializarea si pornirea unui thread: thread
kernel object, alocare Thread environment block ( contine handlere de
eroare + native thread local storage), alocare stack, notificare pentru
toate dll-urile native din process DLL_THREAD_ATTACH => incarcarea in
memorie a paginilor de cod care trateaza aceasta situtatie.
Clr mentine un singur threadpool per proces. Intern se mentine o coada
de task-uri pentru fiecare AppDomain si o coada separata pentru
requesturi native (ASP.NET).
Task-urile din lista sunt rulate in regim FIFO ( nu strict). Listele sunt
protejate de printr-un monitor lock.
Threadpool contine worker threads accesibile prin QueueUserWorkItem,
si IO threads, folosite pentru notificarile operatiilor IO asincrone sau
direct folosind UnsafeQueueNativeOverlapped.
20. ThreadPool algorithms
Clr ajusteaza in mod automat numarul de thread-uri in functie de rata de finalizarea a task-
urilor.
Aceasta autoajustare este dificila datorita multitudinii de factori care afecteaza performanta
threadpool care genereaza un “zgomot” ce face dificila corelarea intre intrari si iesiri.
In NET 4.0 algoritmul a suferit o modificare bazata pe teoria procesarii semnalelor. S-a
considerat numarul de thread-uri ca reprezentand un semnal de intare. Acest semnal este
modificat intentionat de threadpool, urmarindu-se aparitia acestor fluctuatii in semnalul de
iesire ( rata de executie a task-urilor)
Acest algoritm performeaza optim pentru task-uri de durata medie 10ms, bine pentru task-
uri de 250ms.
Threadpool 4.0 folosete intern un ConcurrentQueue<T> care este lock-free si “prietenoasa”
cu GC-ul.
10 milioane de task-uri empty
Computer .net 3.5 .net 4.0 Imbunatatire
Dual core 5 sec 2.45 sec 2x
Quad core 19.5 sec 3.42 sec 6x
24. Limitari ThreadPool
Nu exista notificari de terminare a task-urilor .
Nu exista un mecanism de comunicare a exceptiilor din task-uri catre initiatorul task-ului.
Daca se stie faptul ca un task va avea o durata mare, nu exista modalitate de a indica
threadpool acest lucru. O multitudine de task-uri de durata mare pot duce la blocarea
majoritatii tread-urilor din pool.
In multe cazuri exista o legatura intre task-urile programate pentru rulare asincrona. Daca un
task A asteapta dupa un task B, iar task-ul B inca nu a fost programat pentru rulare, task-ul A
blocheaza in mod inutil thread-ul asteptand dupa task-ul B.
Nu exista posibilitatea de a anula (cancel) un task atunci cand a fost programat dar nu a fost
inca executat si deasemenea nu exista un mecanism standard de semnalizarea a faptului ca
dorim oprirea task-ului
System.Timer.Timer isi executa callback-ul pe un thread din ThreadPool. Acuratetea acestor
evenimente poate fi afectata de incarcarea curenta a thread-urilor din ThreadPool.
Threadpool se acomodeaza relativ greu (0.5s) la cresterea brusca a volumului de task-uri
care trebuie executate. Acest lucru este valabil atat pentru I/O threads cat si pentru worker
threads.
25. Limitari ThreadPool (WCF)
WCF executa request-urile folosind I/O threads si nu worker threads.
In figura se evidentiaza modul in care TP creeaza noi pentru 40 de request-uri in rafala.
Intra in actiune faptul ca TP creeaza un thread nou la 0.5 sec.
Acest burst se configureaza folosind minIOThreads. Din pacate in .NET 3.5 exista un bug care face ca aceasta
limita minima sa nu fie respectata decat pe o durata limitata. Bugfix exista in NET 4.0-4.5
26. Limitari ThreadPool (WCF)
O solutie recomandata de Microsoft este extinderea WCF cu un custom operation behavior care sa ruleze
requesturile WCF folosind worker threads. In cazul worker threads configurarea minWorkerThreads este
respectata in toate cazurile.
27. Task Parallel Library (TPL)
Reprezinta o colectie de clase ce ofera un API mai bogat pentru rularea de task-uri de
granularitate fina optimizate pentru hardware modern (multi core).
Versiunea CTP folosea un scheduler propriu. Modificarile aduse ThreadPool in NET 4.0 au
permis folosirea threadpool ca scheduler default pentru TPL.
TPL permite corelarea intre task-uri: ContinueWith, ContinueWhenAll, ContinueWhenAny.
Deasemenea apelul Task.Wait gestioneaza situatia in mod inteligent: daca task-ul care este
asteptat nu a fost programat inca, el va fi programat pentru rulare, in general inline, pe
thread-ul curent, optimizandu-se utilizarea thread-rilor din pool.
TPL permite utilizarea de scheduleri custom, pentru obtinerea unor pattern-uri de rulare
complexe.
TPL poate utilizeza SynchronizationContext pentru programarea task-urilor care modifica
user interface-ul.( Windows Forms, WPF)
28. Imbunatatiri ThreadPool 4.0/Tasks
In afara de lista globala de task-uri fiecare worker thread isi mentine o lista interna de tipul
WSL ( work stealing queue ). Lista este lock free atunci cand este accesata privat si necesita
sincronizare doar daca este accesata extern.
Task-urile care sunt create in contextul executiei pe un worker thread sunt adaugate in lista
locala a thread-ului. Task-uri create in contextul aplicatiei sunt adaugate in lista globala.
31. Imbunatatiri ThreadPool 4.0/Tasks
Task-urile din lista locala sunt executate in regim LIFO. Probabilitatea ca
datele referite de ultimul task sa se afle in L1, L2 cache este foarte mare si
prin urmare rularea LIFO aduce un plus de performanta.
32. Imbunatatiri ThreadPool 4.0/Tasks
In cazul in care un worker thread este idle, se verifica daca lista globala contine elemente. Daca lista
globala este vida, se verifica listele locale ale celorlalte thread-uri. Daca se gaseste un element in
asteptare este “furat” si alocat pe thread-ul idle. In cazul “furtului” de task-uri, lista locala vecina este
consumata FIFO. Motivul este acela ca de obicei, in cazul algoritmilor recursivi de tip divide et impera,
task-urile mai aproape de varf executa un numar mai mare de operatii.
33. Task, Task<T>
Reprezinta o abstractizare la un nivel mai inalt decat acela de Thread.
Un task poate fi sau nu rulat pe un thread dedicat.
Task-urile pot fi inlantuite folosind continuations, iar executia acestui lant de task-uri poate fi conditionata in
functie de terminarea cu success sau nu a task-ului precedent.
Exista notiunea de child task
Task-urile pot fi rulate pe threadpool, pe thread-ul de UI, sau folosind un custom scheduler.
Task.Factory.StartNew(() => Console.WriteLine("Foo"));
Crearea task-ului este “hot”, el este programat imediat pentru executie.
Apelul Wait blocheza thread-ul curent pana la finalizarea task-ului (vezi Thread.Join ).
var task = Task.Factory.StartNew(() => Thread.Sleep(1000));
Console.WriteLine(task.IsCompleted);
task.Wait();
Putem indica TPL sa porneasca task-ul pe un thread separat si nu pe threadpool:
Task.Factory.StartNew(() => Thread.Sleep(5000), TaskCreationOptions.LongRunning);
Putem indica TPL sa incerce sa ruleze task-urile in ordinea in care au fost create:
Task.Factory.StartNew(()=> Thread.Sleep(5000),TaskCreationOptions.PreferFairness);
34. Parent/Child Task
Apelul Wait pentru parinte se finalizeaza atunci cand toate task-uri copii au fost deasemenea finalizate.
Exceptiile din task-urile subordonate sunt ridicate sub forma unui AggregateException.
var parent = Task.Factory.StartNew(() =>
{
Console.WriteLine ("I am a parent");
Task.Factory.StartNew (() => // Detached task
{
Console.WriteLine ("I am detached");
});
Task.Factory.StartNew (() => // Child task
{
Console.WriteLine ("I am a child");
}, TaskCreationOptions.AttachedToParent);
});
parent.Wait();
35. Wait pentru mai multe task-uri
var t1 = Task.Factory.StartNew(DoOperation1);
var t2 = Task.Factory.StartNew(DoOperation2);
Task.WaitAny(t1, t2);
Task.WaitAll(t1, t2);
WaitAll asteapta toate task-urile din lista, chiar daca
unele s-au terminat cu exceptie. La final, WaitAll
arunca o exceptie de tipul AggregateException.
WaitAll, WaitAny, pot primi parametri suplimentari de
tip TimeSpan sau CancellationToken.
Cancelarea WaitAll nu opreste task-urile care sunt
curent in executie. In cazul cancelarii se va ridica o
AggregateException care poate contine mai multe
TaskCancelledException.
38. Continuations
ContinueWith executa un task dupa ce un alt task este finalizat. Un task se poate termina cu
success, cu eroare sau poate fi cancelat.
Task task1 = Task.Factory.StartNew(() => Console.Write("primul task..."));
Task task2 = task1.ContinueWith(ant => Console.Write("..,continuation"));
ContinueWith intoarce la randul sau un task, permitand inlantuirea mai multor task-uri.
Un task si urmatorul continuation se pot executa pe thread-uri diferite. Putem forta continuation-
ul sa se execute pe acelasi thread folosind flag-ul: TaskContinuationOptions.ExecuteSynchronously.
Se poate obtine un plus de performanta evitandu-se delay-ul si un context switch suplimentar.
Continuation si Task<T>.
Task.Factory.StartNew<int>(() => 8)
.ContinueWith(ant => ant.Result * 2)
.ContinueWith(ant => Math.Sqrt(ant.Result))
.ContinueWith(ant => Console.WriteLine(ant.Result)); // 4
Exceptiile in task-ul precedent pot fi observate in continuation verificand proprietatile Exception,
Result sau apeland Wait() si asteptand AggregateException. Un pattern safe este acela de
propagare a exceptiei:
Task.Factory.StartNew(() => { throw new Exception(); })
.ContinueWith(ant =>{ ant.Wait();
// Continuarea procesarii
});
39. Continuations
O alta modalitate de a trata exceptiile este aceea de a inlantui continuari diferite pentru stari
diferite ale task-ului precedent:
var task1 = Task.Factory.StartNew(() => { throw null; });
var error = task1.ContinueWith(ant =>
Console.Write(ant.Exception),TaskContinuationOptions.OnlyOnFaulted);
var ok = task1.ContinueWith(ant =>
Console.Write("Success!"),TaskContinuationOptions.NotOnFaulted);
Exemplue de metoda de extensie pentru observare si ignorare a exceptiei:
public static void IgnoreExceptions (this Task task)
{
task.ContinueWith(t=>{var ignore=t.Exception;},askContinuationOptions.OnlyOnFaulted);
}
Task.Factory.StartNew (() => { throw null; }).IgnoreExceptions();
40. Gotcha continuations
Daca un continuation nu se executa datorita optiunilor, nu este “ignorata” ci este considerata
cancelled. Prin urmare, toate continuarile urmatoare vor fi executate, cu exceptia cazului in care au
specificat flag-ul TaskContinuationOptions.NotOnCanceled
Task t1 = Task.Factory.StartNew (...);
Task fault = t1.ContinueWith (ant => Console.WriteLine ("fault"),
TaskContinuationOptions.OnlyOnFaulted);
Task t3 = fault.ContinueWith (ant => Console.WriteLine ("t3"));
41. Task si exceptiile
Pornind de la comportamentul default in NET 4.0 prin care orice exceptie netratata intr-un
thread duce la oprirea procesului, in NET 4.0 TPL a introdus notiunea de “observed Exceptions”.
Orice task a carui exceptie nu este “observata” va duce la oprirea procesului. Aceasta verificare
are loc la momentul colectarii task-ului in finalizer.
Observarea exceptiei: Task.Wait sau accesarea proprietatii Exception sau IsFaulted intr-un
continuation.
Exista un handler global: TaskScheduler.UnobservedException. Acolo avem posibilitatea sa
marcam exceptia ca observata.
In .NET 4.5 task-urile au devenit un mecanism standard prin imbogatirea limbajului cu
mecanismele async/await accesibile tuturor tipurilor de utilizatori.
S-a renuntat la obligativitatea observarii exceptiilor. Handlerul
TaskScheduler.UnobservedException va fi apelat totusi pentru fiecare eroare.
Acest comportament este reconfigurabil la modul NET 4.0
42. Exemplu .NET 4.5
Task op1 = PerformOperation1();
Task op2 = PerformOperation2();
await op1;
await op2;
Daca op1 si op2 ridica impreuna exceptie, primul await va propaga exceptia catre codul
apelant, pe cand al doilea nu va avea sansa sa fie observat si va duce mai tarziu la oprirea
procesului.
Comportamentul permisiv cu privire la exceptii din NET 4.5 poate fi schimbat:
<configuration>
<runtime>
<ThrowUnobservedTaskExceptions enabled="true"/>
</runtime>
</configuration>
Se recomanda rularea testelor folosind acest flag setat true.
44. Async/Await
Metoda care va fi rulata asincron trebuie intoarca void, Task, sau Task<T>. Metoda apelanta care foloseste in
interiorul ei apelul await trebuie marcata ca async. Compilatorul ar putea deduce automat acest pattern fara
sa oblige programatorul sa marcheze metodele cu async.
Await foloseste SynchronizationContext pentru a continua pe acelasi thread pe care metoda async a pornit.
Rularea seriala datorata await este valabila doar in contextul metodei care contine apelul.
private Task<int> longRunningAsync()
{
Thread.Sleep(1000);
return Task.FromResult(5);
}
private async void button1_Click_Async(object sender, EventArgs e)
{
this.button1.Enabled = false;
int i = await longRunningAsync();
this.button1.Text = i.ToString(CultureInfo.InvariantCulture);
this.button1.Enabled = true;
}
46. Async/Await
Nu doar pentru obiecte de tipul Task se poate apela await. Se poate apela await pentru orice tip care detine o
metoda cu numele GetAwaiter. Nu exista o interfata ce trebuie implentata, doar o metoda GetAwaiter ce
intoarce un tip cu urmatoarea semnatura de metode: IsCompleted, OnCompleted(Action), GetResult(). Vestea
buna este ca aceste metode pot fi metode de extensie. Putem extinde astfel orice tip ( in masura in care are
sens) pentru a suporta pattern-ul await.
Vom folosi chiar clasa awaiter pe care o intoarce Task.GetAwaiter():
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
public static class AsyncExts
{
public static TaskAwaiter GetAwaiter(this TimeSpan span)
{
return Task.Delay(span).GetAwaiter();
}
public static async void Demo()
{
await TimeSpan.FromMinutes(1);
}
}
47. Async/Await
Await nu se poate aplica unui apel de metoda lista de task-uri. Folosind metode de extensie putem scrie:
public static class AsyncExts2
{
public static TaskAwaiter GetAwaiter(this IEnumerable<Task> tasks)
{
return Task.WhenAll(tasks).GetAwaiter();
}
public static async void Demo()
{
var urls = new[] {"google.com", "msn.com"};
await (from url in urls select DownloadAsync(url));
}
private static async Task<string> DownloadAsync(string url)
{
…. Code …
}
}
48. Async/Await
Putem crea un awaiter pentru orice element care ofera notificari cu privire la finalizarea crearii sale sau la
finalizarea unei actiuni.
public static TaskAwaiter<int> GetAwaiter(this Process process)
{
var tcs = new TaskCompletionSource<int>();
if (process == null)
{ tcs.TrySetResult(-1);
return tcs.Task.GetAwaiter();
}
process.EnableRaisingEvents = true;
process.Exited += (s, e) => tcs.TrySetResult(process.ExitCode);
if (process.HasExited)
tcs.TrySetResult(process.ExitCode);
return tcs.Task.GetAwaiter();
}
public static async void Demo()
{
await Process.Start("notepad.exe");
}
49. Async/Await
Exemplu de awaiter custom: extindem await pentru controale windows forms. Codul care urmeaza apelului
await va fi executat pe UI thread.
public class WindowsFormsAwaiter
{
private readonly Control control;
public WindowsFormsAwaiter(Control control)
{
this.control = control;
}
public bool IsCompleted
{
// daca avem invoke required suntem pe background thread si metoda noastra nu a rulat
get { return !control.InvokeRequired; }
}
public void OnCompleted(Action continuation)
{
//ruleaza continuation pe ui thread
control.BeginInvoke(continuation);
}
public void GetResult() { }
}
50. Async/Await
Exemplu de awaiter custom: extindem await pentru controale windows forms. Codul care urmeaza apelului
await va fi executat pe UI thread.
public static class AsyncExt4
{
public static WindowsFormsAwaiter GetAwaiter(this Control control)
{
return new WindowsFormsAwaiter(control);
}
public static void Test(Label label1)
{
Task.Factory.StartNew(async () =>
{
var text = GetTextFromLongRunningTask();
await (label1);
label1.Text = text; //ruleaza pe ui thread
});
}
51. Exceptii in Task.Wait vs await
In cazul Task.Wait erorile sunt intotdeauna imbracate intr-o AggregateException.
In .NET 4.5 mecanismul de propagare a exceptiilor a fost imbunatatit. A aparut clasa
ExceptionDispatchInfo. Cu ajutorul ei putem capta o exceptie pe un thread si o putem ridica pe
alt thread pastrandu-se intreaga informatie fara sa mai fie necesar sa o imbracam in alta
exceptie. Exceptia e prezentata ca si cum flow-ul executiei ar fi trecut natural de pe thread-ul A
in contextul thread-ului B cu prezervarea stack-ului si al Watson buckets.
Await foloseste acest nou mecanism de ridicare al exceptiilor ridicand prima exceptie aparuta.
Daca se doreste inspectarea tuturor exceptiilor aparute in urma rularii task-ului va trebui
inspectata proprietatea Exception sau apelat GetResult(). Se va optine un AggregateException.
52. Async/Await Gotchas 1
Fie o functie WriteFileAsync
Utilizare:
In unele cazuri codul va functiona, in alte cazuri vor aparea exceptii sau rezultate imprevizibile in functia
care urmeaza WriteFileAsync si depinde de rezultatul executiei acesteia.
54. Async/Await Gotchas 2
Scrierea unui event handler async. Exemplu: in Windows 8 pentru implementarea data sharing intre
aplicatii, trebuie tratat evenimentul DataTransferManager.DataRequested
Folosind un handler ca cel de mai sus, aplicatia nu va functiona in regim de sharing.
Solutia: indepartarea comportamentului asincron: async await.
55. Async/Await Gotchas 3
Tentatia de a apela metoda Wait() avand ca argument un Task returnat de o metoda async. Rezultatul
poate fi un deadlock.
public Result GetResult()
{
doWorkAsync().Wait(); //DEADLOCK!
return CreateResult();
}
private async Task doWorkAsync()
{
var task = Task.Delay(500);
}
Apelul doWorkAsync capteaza SynchronizationContext-ul curent. La finalizare va incerca sa intoarca
controlul thread-ului de UI ( in Windows Forms, WPF ). Din pacate, thread-ul de UI este blocat
asteptand finalizarea task-ului.
56. Parallel programming PFX
In cazul computerelor de azi avem in mod obisnuit mai multe core-uri. Pentru a beneficia de intreaga
putere de procesare trebuie sa parcurgem urmatorii pasi:
Partitionare optima a setului de date de intrare
Executarea in paralel a acestor seturi pe un numar de thread-uri ales cu grija in functie de core-urile disponibile
Agregarea resultatelor partiale pe fiecare thread in parte si in rezultatul final folosind structuri de date lock free
57. Parallel Linq (PLINQ)
Plinq reprezinta cel mai facil mod de paralelizare a task-urilor.
Se foloseste extensia AsParallel() si apoi body-ul query-ului linq ramane neschimbat.
//calculez numerele prime mai mici decat 100000
var parallelQuery = from n in Enumerable.Range(3,1000000).AsParallel()
where Enumerable.Range(2, (int)Math.Sqrt(n)).All(i => n % i > 0)
select n;
int[] primes = parallelQuery.ToArray();
Apelul AsParallel() intoarce un ParallelQuery<T>(), toti operatorii Linq ce urmeaza se refera la
aceasta implementare. Pentru revenirea la rularea secventiala si la operatorii standard Linq se va apela
extensia AsSequential();
In cazul iterarii resultatului unui linq query secvential obisnuit, rezultatele se obtin printr-o metoda pull
dictata de client. In cazul unui query paralel, firele de executie calculeaza in avans o parte din rezultate
pe care le stocheaza intr-un buffer intern, de unde le serveste client-ului care itereaza rezultatul. Daca
clientul opreste iterarea, PLINQ opreste si el procesarea paralela pentru optimizarea consumului de
resurse ( CPU/Memory).
Buffering-ul intern vine in trei flavor-uri:
AutoBuffered
NotBuffered - util atunci cand dorim sa primim rezultatele cat mai repede posibil
FullyBuffered - folosit implict atunci cand apelam OrderBy, Reverse sau Aggregate
58. PLINQ si ordonarea
Un efect colateral al paralelizarii executiei este acela ca rezultatul obtinut
dupa reuniunea rezultatelor partiale nu mai respecta ordinea setului de intrare.
Daca se doreste mentinerea ordinii initiale se foloseste AsOrdered(), daca nu mai
este necesara mentinerea ordinii se poate reveni la comportamentul aleator, mai
performant, folosind AsUnOrdered()
inputSequence
.AsParallel()
.AsOrdered() // Mentine ordinea
.QueryOperator1()
.QueryOperator2()
.AsUnordered() //De aici ordinea nu mai este mentinuta
.QueryOperator3();
AsOrdered() este mai putin performant, AsUnordered() este comportamentul implicit.
59. PLINQ Cancellation
var million = Enumerable.Range(3, 1000000);
var cancelSource = new CancellationTokenSource();
var primeNumberQuery =
from n in million.AsParallel()
.WithCancellation(cancelSource.Token)
where Enumerable.Range(2, (int) Math.Sqrt(n)).All(i => n%i > 0)
select n;
Task.Delay(1000)
.ContinueWith(t => cancelSource.Cancel()); //cancel query after 1000 ms
try
{
// Start query
var primes = primeNumberQuery.ToArray();
}
catch (OperationCanceledException)
{
Console.WriteLine ("Query canceled");
}
60. PLINQ Optimizari
Unul din avantajele PLINQ este acela ca se ocupa de agregarea partitiilor in lista
finala. De multe ori dorim doar executia unei procesari in paralel asupra tuturor
elementelor.
O optimizare o ofera metoda de extensie ForAll:
"abcdef".AsParallel().Select(char.ToUpper).ForAll(Console.Write);
Atunci cand intalneste apelul ForAll, PLINQ nu mai agrega partitiile in rezultatul
final ci ruleaza procesarea direct asupra elementelor din partitii.
Operatia de agregare poate deveni costisitoare atunci cand vorbim de un numar
foarte mare de elemente.
61. PLINQ modalitati de partitionare
Range partitioning
Functioneaza cu surse de date care implementeaza Ilist, IList<T>. Dau randament optim atunci cand
prelucrarile pentru fiecare element au durate apropiate.
Chunk partitioning
Este folosita in cazul unor surse de date non indexabile care implementeaza doar IEnumerable.
Este o partitionare dinamica, se incearca optimizare globala. Mai intai se aloca un numar mic de
elemente in fiecare partitie, apoi pe masura ce apar noi elemente in sursa de date se dubleaza numarul
de elemente alocat pe fiecare partitie. Se asigura partitionarea egala si atunci cand lista are un
numar mic de elemente dar si cand numarul este foarte mare.
Schema poate fi avantajoasa si atunci cand prelucrarile asupra fiecarui element sursa au durate mult
diferite. Daca un chunk a fost prelucrat complet, algoritmul aloca elementele disponibile din sursa,
asigurandu-se o incarcare constanta. In cazul range partitioning, daca o partitie a fost prelucrata
mai rapid, worker-ul ramane nefolosit.
Striped partitioning
Este un caz particular al range partitioning folosit in cazul operatorilor TakeWhile, SkipWhile. Daca
sunt doi workeri unul primeste elementele pare altul cele impare.
Hashed partitioning
Cea mai costisitoare schema. Folosita in cazul operatorilor: Join, GroupJoin, GroupBy, Distinct,
Except, Union, Intersect. Elementele sunt alocate in partitii pe baza hash-ului.
62. PLINQ
LIMITARI
Functioneaza doar pentru provideri linq locali. Nu functioneaza pentru LinqToSql, EF sau alti
provideri remote.
Daca se acceseaza alte variabile externe din query este necesara folosirea un or primitive de blocare
(lock), lucru care poate aduce un impact performantei. Se recomanda structurile de date lock free.
GOTCHAS
Limitarea apelului de metode thread-safe gen Console.WriteLine in interiorul PLINQ. Console.WriteLine
foloseste un lock si forteaza un acces secvential, incetinind rularea query-urilor PLINQ
Paralelizarea excesiva, poate afecta performanta in mod advers:
var q = from cust in customers.AsParallel()
from order in cust.Orders.AsParallel()
where order.OrderDate > date
select new { cust, order };
Cust.Orders are un numar mare de elemente/ Operatia aplicata order este una complicata / Exista
sufieciente core-uri.
Ordonari inutile
Atentie la accesul controalelor WPF, Windows Forms din PLINQ. ( thread affinity )
A nu se incerca sincronizarea manuala intre threadurile partitiilor sau iteratiilor.
63. Parallel.For si Parallel.Foreach
Folosirea index-ului curent
Parallel.ForEach ("Hello, world", (c, state, i) =>
{
Console.WriteLine (c.ToString() + i);
});
Iesirea din bucla paralela
foreach (char c in "Hello, world")
if (c == ',')
break;
else
Console.Write (c);
Parallel.ForEach ("Hello, world", (c, loopState) =>
{
if (c == ',')
loopState.Break();
else
Console.Write (c);
});
64. Parallel.For si Parallel.Foreach
Oprirea completa
Parallel.ForEach ("Hello, world", (c, loopState) =>
{
if (c == ',')
loopState.Stop();
else
Console.Write (c);
});
Cand pe alta partitie avem eroare sau a fost invocat Stop sau Break putem optimiza
si opri iteratia curenta verificand: loopState.ShouldExitCurrentIteration
Erorile sunt impachetate la final intr-un AggregateException.
65. Parallel.For si Parallel.Foreach
Optimizarea concatenarii rezultatelor buclei folosind variabile locale pentru
fiecare partitie (se evita folosirea primitivelor de locking)
var locker = new object();
double grandTotal = 0;
Parallel.For (1, 10000000,
() => 0.0, // Initializare variabila locala per partitie
(i, state, localTotal) => // Corpul functiei de procesare pe partitie.
localTotal + Math.Sqrt (i), // returneaza noua valoare locala
localTotal => // functia de concatenare a rezultatelor partitiilor
{ lock (locker) grandTotal += localTotal; } // aici trebuie folosita blocarea
);
Avantajul este ca blocarea se realizeaza doar la concatenarea finala, nu si pentru
fiecare iteratie.