субота, 09. април 2016.

Rad sa nitima u C# programskom jeziku


Skoro svaki korisnik PC računara kod svoje kuće koristi računar sa jednim CPU – Central Processing Unit – centralnim procesorom, koji u suštini obavlja samo jednu operaciju u datom vremenu. Zbog velike brzine procesora, mi često imamo utisak da na primer računar obavlja više poslova istovremeno kad otvorimo više aplikacija u Windows-u. Međutim, činjenica je dok se jedna operacija obavlja, druge čekaju. Na primer; kad vam se pojavi neki veliki bag u računaru, to često znači da se neka operacija na procesorskom nivou izvršava previše vremena ili se ne može izvršiti do kraja i zbog toga vam tada izgleda da vam je celi Windows smrznut i slika na monitoru ukočena, sve dok ne resetujete računar.


( Životni ciklus niti u C# programskom jeziku )

Da se ovo nebi stalno dešavalo da svaka aplikacija zamrzne celi Windows pri svakoj komplikaciji tokom rada, stvoren je koncept korišćenja niti. Svaka aplikacija u Windows-u pokreće svoj vlastiti proces koji je izolovan od drugih aplikacija. Znači, svaki proces pokreće vlastiti thread – nit; nit shvatite jednostavno kao virtualni CPU. Nit je sekvenca izvršavanja programa. Većina programa koje programirate u C# programskom jeziku ima jednu ulaznu tačku, to je metoda Main() i završava se povratkom iz metode. Međutim, ni jedan Internet pretraživač ne radi na taj način. Oni kao da izvršavaju više zadataka istovremeno. I vi ćete dolaziti u situacije u vašim programima da vam je potrebno da obavljate neke funkcije bar prividno istovremeno. Svaka nit u Windows-u ima svoje određeno vreme koje se meri u milisekundima i nakon tog perioda nit se pauzira i Windows se prebacuje na drugu nit. Takvo ponašanje zovemo Context Switching. Koristeći niti mi izvršavamo procese bez čekanja da se prethodni proces završi. Na taj način mi samo stvaramo iluziju da se izvršava više niti u isto vreme. Kod više procesorskih računara gde imamo više CPU, koristimo Parallelism – paralelizam; gde se niti izvršavaju paralelno na više različitih CPU. Na koje sve načine možete da izvršavate Multithreating – izvršavanje više niti; najbolje da vidite šta vam .Net Framework nudi.

Kako se kreira nit koristeći Thread klasu?


Da bi ste najjednostavnije manipulisali nitima koristite Thread klasu koja se nalazi u System.Threading imenskom prostoru. Instanca klase Thread predstavlja jednu nit; jednu sekvencu izvršavanja. Drugu nit možete da formirate tako što konkretizujete objekat niti. Klasa Thread vam jednostavno omogućava da kreirate nove niti, kontrolišete njihova svojstva i dobijate status niti. Opet treba da znate da niti treba da koristite samo ako imate dobar razlog za to. Pogledajte praktičan primer kako se pravi nit.

using System;
using static System.Console;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ThreadClass
{
    class Program
    {
        public static void ThreadMethod()
        {
            for (sbyte i = 1; i < 5; i++)
            {
                WriteLine($"A: {i + 1}");
                Thread.Sleep(0);
                
            }
        }

        static void Main(string[] args)
        {
            Thread t = new Thread(new ThreadStart(ThreadMethod));
            t.Start();

            for (sbyte i = 1; i < 5; i++)
            {
                WriteLine($"B: {i + 1}");
                Thread.Sleep(0);

            }

            t.Join();

            WriteLine(Environment.NewLine + "Press any key to continue...");
            ReadKey();

        }
    }
}

Da u navedenom primeru nismo koristili niti, prvo bi se izvršila petlja for iz ThraedMethod metode zatim for petlja iz Main metode. To znači, da bi petlja for iz Main metode morala da sačeka da se petlja for iz ThreadMethod metode završi do kraja. Zamislite da metoda ThreadMethod radi neki kompleksniji zadatak za koje je potrebno mnogo vremena. Celi vaš program bi morao da čeka da se metoda ThreadMethod završi da bi program nastavio sa radom. Za to vreme celi vaš program bi bio neupotrebljiv dok bi korisnik vašeg programa imao utisak da nešto nije u redu sa vašim programom. Upotrebom niti se jednostavno omogućava da se niti izvršavaju sinhronizovano. Prvo obratite pažnju kako se kreira nit. On zahteva kao argument da unesete metodu. Ako se pitate kako je to moguće, to je moguće zahvaljujući delegatima. Klasa System.Threading sadrži delegat koji omogućava da argument zahteva metodu. Kad ste kreirali nit, ona ništa ne radi sve dok ne pokrenete nit metodom Start(). Inače nit možete pokrenuti, suspendovati, nastaviti ili prekinuti. Zatim, pogledajte metodu Sleep(0). Nula je u ovom slučaju vreme u milisekundama. Navedena naredba obaveštava Windows da odmah pređe na sledeću nit bez čekanja, dok metoda Join() omogućava da nastavite obradu podataka kad nit bude uništena. Kad pokrenete navedeni kod, rezultat će biti sličan:

B: 1
A: 1
B: 2
A: 2
B: 3
A: 3
B: 4
A: 4
B: 5
A: 5

Press any key to continue...


Kako navedeni program izgleda možete pogledati i na video-u:



( C# 6.0 Tutorial - Advanced - 6. Thread Class )

Kako da kreiram nit u background-u?

Postoje dve vrste niti, foreground thread – nit koja se izvršava u prvom planu i background thread -  nit koja se izvršava u pozadini. Mnogo je bitno znati razliku. Po defaultu sve niti se izvršavaju u prvom planu i to jednostavno znači da se vaša aplikacija ne može ugasiti dok se sve niti ne izvrše. Ukoliko ste kreirali nit koja radi u pozadini, vaša aplikacija se može ugasiti i ako nit nije izvršena. Da bi se vaša nit izvršavala u pozadini dovoljno je podesiti properti IsBackground na true pre pokretanja niti. Pogledajte sledeći program.

using System;
using static System.Console;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ThreadBackground
{
    class Program
    {
        public static void ThreadMethod()
        {
            for (sbyte i = 1; i < 5; i++)
            {
                WriteLine($"Process: {i + 1}");
                Thread.Sleep(1000);

            }
        }

        static void Main(string[] args)
        {
            Thread t = new Thread(new ThreadStart(ThreadMethod));
            t.IsBackground = true; // the application won't wait until the background threads are completed
            t.Start();

            // WriteLine(Environment.NewLine + "Press any key to continue..." + Environment.NewLine);
            // ReadKey();

        }
    }
}

Kad pokrenete navedeni kod, program će se pokrenuti i odmah zatvoriti dok će nit i posle zatvaranja aplikacije nastaviti da se izvršava. Kako navedeni program izgleda možete pogledati i na video-u:



( C# 6.0 Tutorial - Advanced - 7. Thread Background )

Kako da kreiram parametorizovan start niti?

Ukoliko vam je potrebno da vi parametrima utičete na izvršavanje niti, to je moguće jer konstruktor Thread se može nadjačati delegatom ParameterizedThreadStart. Pogledajte sledeći kod:

using System;
using static System.Console;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ParameterizedThread
{
    class Program
    {
        public static void ThreadMethod(object o)
        {
            for (int i = 0; i < (int)o; i++)
            {
                WriteLine($"Process: {i + 1}");
                Thread.Sleep(1000);

            }
        }

        static void Main(string[] args)
        {
            Thread t = new Thread(new ParameterizedThreadStart(ThreadMethod));
                     
            t.Start(5);
            t.Join();
                       
            WriteLine(Environment.NewLine + "Press any key to continue...");
            ReadKey();

        }
    }
}

Kad pokrenete navedeni program, rezultat će biti sledeći:

Process: 1
Process: 2
Process: 3
Process: 4
Process: 5

Press any key to continue...


Kako navedeni program izgleda možete pogledati i na video-u:



( C# 6.0 Tutorial - Advanced - 8. ParameterizedThreadStart )

Kako da zaustavim nit?

Da bi ste zaustavili nit vi možete koristiti Abort() metod, ali pošto ovu nit izvršavate preko druge niti to može izazvati grešku, tako da je najbolje rešenje i praksa da koristite deljenu promenjivu. Nemojte da vas zbunjuje pisanje lambda izraza za skraćeno pisanje verzije delegata. Za sada samo obratite pažnju na koje sve načine može skraćeno da se kodira i kako to rade profesionalci u programiranju i pokušajte da nešto od toga i sami usvojite u vašem kodiranju. Pogledajte sledeći kod:

using System;
using static System.Console;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace StoppingThread
{
    class Program
    {
        static void Main(string[] args)
        {
            bool stopped = false;

            Thread t = new Thread(new ThreadStart(() =>
            {
                while (!stopped)
                {
                    WriteLine("Running...");
                    Thread.Sleep(2000);
                }

            }));

            t.Start();
          
            WriteLine("Press any key to continue..." + Environment.NewLine);
            ReadKey();

            stopped = true;
            t.Join();

        }
    }
}

Kad pokrenete navedeni program, pritisnete bilo koji taster na tastaturi i preko promenjive stopped će se obustaviti izvršavanje petlje i sa njom nit.

Press any key to continue...

Running...
Running...
Running...
Running...
Running...
Running...
...


Kako navedeni program izgleda možete pogledati i na video-u:


( C# 6.0 Tutorial - Advanced - 9. Stopping Thread )

Da li mogu da koristim atribut umesto deljene promenjive?

Možete. Čak je i praksa da se koristi atribut ThreadStatic umesto deljene promenjive koji imamo zahvaljujući klasi ThreadStaticAttribute. Pogledajte sledeći kod koji omogućava svakoj niti njenu vlastitu kopiju polja.

using System;
using static System.Console;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace UsingThreadStaticAttribute
{
    class Program
    {
        [ThreadStatic]
        public static int field;

        static void Main(string[] args)
        {
            new Thread(() =>
            {
                for (sbyte i = 0; i < 5; i++)
                {
                    field++;
                    WriteLine($"A: {field}");

                }

            }).Start();

            new Thread(() =>
            {
                for (sbyte i = 0; i < 5; i++)
                {
                    field++;
                    WriteLine($"B: {field}");

                }

            }).Start();

            WriteLine(Environment.NewLine + "Press any key to continue..." + Environment.NewLine);
            ReadKey();

        }
    }
}

Kad pokrenete navedeni program, vaš rezultat će biti sličan:

A: 1
A: 2
A: 3
A: 4
A: 5

Press any key to continue...

B: 1
B: 2
B: 3
B: 4
B: 5


Obratite pažnju da bez atributa ThreadStatic, naše statično polje field bi maksimalno umesto 5, iznosilo 10.

A: 1
A: 2
A: 3
A: 4
A: 5
B: 6

Press any key to continue...

B: 7
B: 8
B: 9
B: 10


Kako navedeni program izgleda možete pogledati i na video-u:


( C# 6.0 Tutorial - Advanced - 8. ParameterizedThreadStart )

Šta je ThreadLocal<T> ?

Ako želite da koristite lokalne podatke i inicijalizujete ih za svaku nit posebno, onda možete koristiti klasu ThreadLocal<T> . U ovom slučaju klasa zahteva delegat od metode koja inicijalizuje vrednost. Pogledajte sledeći primer.

using System;
using static System.Console;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace UsingThreadLocal
{
    class Program
    {
        public static ThreadLocal<int> field =
            new ThreadLocal<int>(() =>
            {
                return Thread.CurrentThread.ManagedThreadId;
                               
            });

        static void Main(string[] args)
        {
            new Thread(() =>
            {
                for (sbyte i = 0; i < field.Value; i++)
                {
                   WriteLine($"A: {i}");

                }

            }).Start();

            new Thread(() =>
            {
                for (sbyte i = 0; i < field.Value; i++)
                {
                    WriteLine($"B: {i}");

                }

            }).Start();

            WriteLine(Environment.NewLine + "Press any key to continue..." + Environment.NewLine);
            ReadKey();

        }
    }
}

Kao što možete videti vi možete koristiti CurrentThread properti da tražite informacije o konkretnoj niti koja se izvršava. To se u programiranju zove Thread’s execution context. Ovaj properti vam daje pristup drugim propertijima poput CultureInfo, prioritetima niti, bezbednosnom sadržaju i drugim korisnim informacijama. U navedenom kodu se CurrentThread kopira iz roditeljske niti u nove niti prenoseći iste privilegije koje ima roditelj nit. Međutim navedeno kopiranje košta, što bi se programerski reklo za koristi previše memorije. Tako ako vam nisu potrebne navedene informacije, ovako ponašanje možete isključiti koristeći ExecutionContext.SupperssFlow metodu. Kad pokrenete navedeni kod rezultat će biti sličan.

A: 0
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
A: 9
A: 10

Press any key to continue...

B: 0
B: 1
B: 2
B: 3
B: 4
B: 5
B: 6
B: 7
B: 8
B: 9
B: 10
B: 11


Kako navedeni program izgleda možete pogledati i na video-u:


( C# 6.0 Tutorial - Advanced - 11. ThreadLocal )

Šta je Thread pool?

Kada radite direktno sa Thread klasom, vi kreirate novu nit svaki put i nit umre kad završite sa njom. Međutim takvo ponašanje takođe može da uzima dosta vremena i memorije. ThreadPool je kreiran da produži niti. Umesto da nit umre kad bude završena, vi je šaljete ponovo u ThreadPool odakle može biti ponovo korišćena. Kad koristite ThreadPool vi stavljate u red niti gde se izvršava nit prema svojoj dostupnosti. Zato što je ThreadPool brojčano limitiran on se ne koristi mnogo ali svakako ima svojih prednosti i vrlo se jednostavno kodira.


using System;
using static System.Console;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace UsingThreadPool
{
    class Program
    {
      
        static void Main(string[] args)
        {
            ThreadPool.QueueUserWorkItem((s) =>
            {
                WriteLine("Working on a thread from the threadpool");

            });

            WriteLine(Environment.NewLine + "Press any key to continue..." + Environment.NewLine);
            ReadKey();

        }
    }
}

Kad pokrenete navedeni program, rezultat će sledeći:

Press any key to continue...

Working on a thread from the threadpool!


Kako navedeni program izgleda možete pogledati i na video-u:


(  C# 6.0 Tutorial - Advanced - 12. ThreadPool )