ParallelFX - optimizacija .NET koda za multi-core procesore II dio
Donosimo vam drugi dio u okviru serije članaka o optimizaciji .NET koda za višejezgrene procesore.
Stop i Break paralelnih petlji
Kod paralelnih implementacija nije moguće koristiti break naredbu za prekid petlje, koja inače zaustavlja regularne C# petlje (poput for, foreach, do, while i sl.). Kada bi koristili break naredbu u paralelnoj petlji zaustavili bi samo jednu petlju, dok bi ostale radile sve dok uslov za prekid pelje ne bi bio ispoštovan u svakoj respektivno.
Teoretski je moguće da se uslov ispuni samo u jednoj petlji dok bi se ostale izvršavale onoliko koliko postoji početno definisanih iteracija. U ovakvim uslovima koristimo preklopljenu For metodu koja sadrži argument ParallelLoopState, odnosno klasu koja implementira prekid paralelni petlji.
Postoje dvije metode kojem nam koriste za prekid paralelnih petlji i to: Break i Stop metoda.
Kada koristimo metodu Break tada će se prekinuti sve iteracije čiji je indeks veći od tekućeg koji poziva Break metodu. Sljedeći primjer se sastoji u tome da se ispišu brojevi od 1 do 20. U koliko se dođe do indexa koji je veći ili jednak 13 u petlji se poziva metoda Break().
static void Main(string[] args){ List<int> list = new List<int>(); Parallel.For(0, 20, (i, loopState) => { if (i >=13) { loopState.Break(); return; } Procesuiranje(); lock (list) { list.Add(i); } } ); foreach (int i in list) { Console.WriteLine(i); } Console.WriteLine("List count: {0}", list.Count); Console.Read(); //Press any key to continue... Console.Read();}static void Procesuiranje(){ Thread.Sleep(10);}
Vidimo da su se se iteracije indeksa manjeg od 13 izvršile, a u listu je ukupno ubačeno 13 elemenata. Sada pogledajmo isti primjer ali sada pozivamo metodu Stop().
static void Main(string[] args){ Console.Title = "Članak za www.itpro.ba"; List<int> list = new List<int>(); Parallel.For(0, 20, (i, loopState) => { if (i >=13) { loopState.Stop(); return; } Procesuiranje(); lock (list) { list.Add(i); } } ); foreach (int i in list) { Console.WriteLine(i); } Console.WriteLine("List count: {0}", list.Count); Console.Read(); //Press any key to continue... Console.Read();}static void Procesuiranje(){ Thread.Sleep(10);}
Ovdje se primjećuje suštinska razlika između ove dvije metode. U slučaju Stop metoda sve naredne iteracije su automatski zaustavljene, pa je broj elemenata u listi 3. Prilikom pokretanja ovog primjera moguće je da dobijete različit izlaz na konzolu jer je procesiranje elemenata u listi različito.
U zadnja dva primjera koristili smo preklopljenu For metodu, koja uzima ParallLoopState klasu kao argument anonimne metode, iz koje smo pozivali metode Break i Stop. Ovom klasom kontrolišemo prekid svih iteracija koje se dešavaju paralelno.
Ovdje je važno napomenutu da je ParallelLoopState klasa u ParallelFX definisana i kao generička klasa koja ima više namjena. Generički klasu ParallelLoopState<> koristimo kada želimo da dijelimo informacije između paralelnih petlji, kao i da provjeravamo statuse petlji. Više o ovoj klasi možete pronaći na MSDN stranici.
Interakcija između paralelnih petlji
Kod paralelnog izvršavanje ponekad je potrebno da izmjenjujemo određene informacije, da bi ostvarili ili riješili određene zadatke. Jedan od takvih zadataka je i paralelna implementacija sume niza članova u kolekciji. Sljedeći listing prikazuje primjer kako razmjenjivati informacije između paralelnih petlji u smislu izračunavanje zbira svih članova nekog polja brojeva. Npr. želimo da izračunamo sumu kvadrata članova niza od 1-10. Implementacija je ponuđena na sljedećem listingu:
using System.Threading.Tasks;using System.Threading;namespace ParallelFXDemo1_ItPro{class Program{ static void Main(string[] args) { //Broj operacija u petljama int[] n = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int sum = 0; Parallel.For(0, n.Length, //definisanje početne vrijednosti za svaki nit () => { return 0; }, //inicijalna vrijednost proslijeđena svakoj niti //POstupak sabiranja (i,/*ParallelLoopState*/ ps, initial) =>//suma inicijalne vrijednosti i tekućeg člana { initial += n[i]; return initial; }, //Sumiranje svih parcijalnih suma koje se nalaze u nitima (parSum) => { Interlocked.Add(ref sum, parSum); }//Suma parcijalnih suma u jednu varijablu sum. ); //Izlaz sume nizova Console.WriteLine("Suma članova S={0}", sum); //Press any key to continue... Console.ReadKey(); }}}Ovaj primjer prikazuje svu jednostavnost TPL-a, koja na elegantan način rješava poprilično složen proces paralelne implementacija sumiranja kroz petlje. Na skoro identičan način, prethodne implementacije korištenjem metode For mogu se primijeniti na metodu ForEach.
Thread-Safe implementacija
Svaka implementacija paralelnog programiranja za sobom povlači niz posljedica o kojima se mora voditi računa, a svakako jedna od njih je, slučaj da se iz dvije niti pristupa jednom objektu u isto vrijeme, ili popularnije da ne postignemo DeadLock, ili DataRace slučaj kada dvije niti u isto vrijeme mijenjaju vrijednost jednoj varijabli čije mijenjanje zavisi od tekuće vrijednosti.
Korištenjem kolekcija, varijabli i drugih objekata potencijalno postoji opasnosti da dođe do DeadLock, odnosno DataRace. U ovakvim slučajevima potrebno je svaki od objekata koji koristimo u paralelnim petljama obezbjediti da bude Thread-Safe, odnosno da pristup i promjena varijable bude neovisna od broja niti koje u isto vrijeme pristupaju objektu.
ParallelFx, uključuje modificirane verzije standardnih .NET kolekcija specijalno namijenjene paralelnom programiranju ili Thread Safe Collection, sihronizacijske i koordinacijske tipove, koji će uveliko pojednostavljivati korištenje ParallelFX i obezbijediti siguran pristup objektima iz više niti odjednom. Oko ovih kolekcija možete saznati više na MSDN-u.
Svaki objekat možemo učiniti Thread-Safe na jednostavan način korištenjem ključne riječi lock. Medjutim, kada koristimo ovaj način zaključavanja gubimo na performansama.
Sljedeći primjer pokazuje kako jednu klasu koja nije Thread-Safe koristiti u paralelnim implementacijama. Navedimo primjer Random klase za generiranja slučajnih brojeva, koju nije sigurno koristiti u paralelnim implementacijama.
/// <summary>/// Thread Safe Generator slucajnih brojeva. Obzirom da klasa /// Random nije ThreadSafe potrebno je zakljucati generiranje /// slucajnog broja prije samog generiranja/// </summary>public class ThreadSafeRandom{ // private static Random random; private static Random random; public ThreadSafeRandom() { //random = new Random((int)DateTime.Now.Ticks); random = new Random (); } public ThreadSafeRandom(int tick) { //random = new Random(tick); random = new Random (tick); } public int Next() { lock (random) { return random.Next(); } } public int Next(int maxValue) { lock (random) { return random.Next(maxValue); } } public int Next(int minValue, int maxValue) { lock (random) { return random.Next(minValue, maxValue); } } public void NextBytes(byte[] buffer) { lock (random) { random.NextBytes(buffer); } } public double NextDouble() { lock (random) { return random.NextDouble(); } }}
Iz primjera vidimo da smo enkapsulirali klasični klasu Random koristeći lock.
Klasa Task
Najvažniji dio ParalleFX biblioteke predstavlja klasa Task i njene varijante Task<TResult>, TaskFactory i TaskFactory<TResult>. U biti cijela prethodna implementacija paralelnih petlji For, Foreach i Invoke implementirana je u ParallelFx pomoću ove klase. Task klasa predstavlja osnovni pojam ParallelFX biblioteke jer je task, jedinica za procesuiranje u ParallelFx, kao i u smislu budućih C# proširenja poput asinhronog programiranja koje će također biti tema serije ovih članaka.
Task predstavlja radnju u toku ili ongoing operation.
S druge strane svaka instanca Task objekta ne povlači za sobom formiranje nove radne niti u čemu je i suština optimizacije procesuiranja na multi-core procesorima. U nekom od narednih članaka govort ićemo oko asinhronog programiranja koji predstavlja evolucijski nastavak u razvoju ParallelFx biblioteke i svaku asinhronu operaciju čine skup Task objekata koji se izvršavaju tako da ne blokiraju glavnu nit aplikacije.
Formiranje objekta Task
Postoji nekoliko načina formiranje objekta klase Task, a jedna od vrlo čestih je preko statičke klase TaskFactory.
var t = Task.Factory.StartNew(() => { //Operacija koju task treba da procesuira});
var t = new Task(() => { //Operacija koju task treba da procesuira}); // task je formiran ali njegovo startanje se posebno poziva t.Start(); // pokretanje/startanje operacije
TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();Task<int> t1 = tcs.Task;Kada implicitno ili eksplicitno pokrenemo operaciju koju task objekat sadrži, potrebno je imati informaciju kad se završava operacija i šta dalje činiti s njom. Čekanje na Task možemo vršiti na više načina zavisno od logike implementacije, funkcije i i sl.
Osnovna metoda za čekanje za završetak taska je metoda Wait().
var t = Task.Factory.StartNew(() => { //Operacija koju task treba da procesuira }); //Čekanje da task završi procesuiranje t.Wait();Task t1 = Task.Create(...); Task t2 = Task.Create(...); Task t3 = Task.Create(...); ... Task.WaitAll(t1, t2, t3); // ili Task.WaitAny(t1, t2, t3)Razlika u metodama WaitAll i WaitAny je očita i nije potrebno posebno obrazlagati.
ParallelFX biblioteka u svojim implementacijama sadrži i mogućnost da jedan ili više taskova formiraju svoje podtaskove i samim tim i relacije roditelj-potomak. Takav način implementacije daje veliku fleksibilnost ovoj biblioteci i daje velike mogućnosti za razne implementacije paralelizma. Uzmimo jedan primjer: roditeljski task u narednom primjeru sačekat će dok se svi njegovi potomci izvrše i tek onda nastaviti sa implementacijom.
Task p = Task.Create(delegate { Task c1 = Task.Create(...); Task c2 = Task.Create(...); Task c3 = Task.Create(...); }); ... p.Wait(); // task p će sačekati svoje potomke c1,c2,c3 da se izvrše da bi se on završioprivate void button1_Click(object sender, RoutedEventArgs e) { Task<string> s = LoadStringAsync(); s.ContinueWith(delegate { Dispatcher.BeginInvoke(new Action(delegate { textBox1.Text = s.Result; })); }); }






Komentari (0)