ParallelFX - optimizacija .NET koda za multi-core procesore II dio

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);
}
 
Kad se ovaj primjer pokrene dobijemo sljedeći izlaz na konzolu:
 
                                

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);
}
 
Izlaz na konzolu je sličan sljedećem:
 
                                                                        

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();
    }
}
}
 
Na koji način prethodni primjer radi? Obzirom da se For metoda dijeli između niti, sum varijabla se mora u svakoj niti izračunati posebno. Kada se izračunaju sve parcijalne sume u svakoj niti, zadnjom anonimnom metodom objedinjavamo sve parcijalne sume i dobijamo konačnu sumu.

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
});
 
Gornjim primjerom objekat Task se formira i pokreće. Međutim, ukoliko želimo formirati task, a njegovo izvršavanje odnosno startanje odgoditi kasnije u kodu, to ćemo uraditi sljedećim listingom:

 

var t = new Task(() => 
    //Operacija koju task treba da procesuira
}); 
// task je formiran ali njegovo startanje se posebno poziva 
t.Start(); // pokretanje/startanje operacije
 
Task možemo formirati i preko TaskCompleationSource<TResult> tako da cijeli proces formiranja task objekta radimo preko CompleationSource instance na sljedeći način:

 

TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
Task<int> t1 = tcs.Task;
 
Čekanje završetka Task operacije

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();
 
Ili ako čekamo više task operacija, imamo nekoliko mogućnosti.
 
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šio
 
Posljednja metoda klase Task koja je ContinueWith metoda koja čeka da se završi jedna ili više task operacija te kontinualno nastavi sljedeću radnju. Pogledajmo sljedeći kod:
 
private void button1_Click(object sender, RoutedEventArgs e)
    {
        Task<string> s = LoadStringAsync();
        s.ContinueWith(delegate
        {
            Dispatcher.BeginInvoke(new Action(delegate
            {
                textBox1.Text = s.Result;
            }));
        });
    }
 
Ako želimo da osvježimo text kontrolu koja se nalazi na nekoj formi, a tekst predstavlja vrlo dugu operaciju, moguće je paralelno izvršiti postupak dobijanja stringa te kad se završi taj proces, kontinualno nastaviti sa operacijom i osvježavanje teksta u kontroli. Ovdje se koristi Dispatcher i BeginInvoke metoda, jer kontinualna operacija također započinje asinhrono i postoji velika mogućnost da se operacija osvježavanje teksta ne vrši iz UI niti, što po pravilu ima za posljedicu rušenja aplikacije, jer kao što je poznato pristup Windows kontrolama odnosno GUI komponentama, vrši isključivo iz glavne ili UI niti.

Komentari (0)

Pošalji svoj komentar