Thread
properties
- bool IsAlive - vrací hodnotu signalizující zda-li vlákno žije
- bool isBackground - vrací hodnotu signalizující zda.li běží vlákno na pozadí
- int ManagedThreadId - vrací id aktuálního vlákno
- string Name - vrací či nastavuje název vlákna
- ThreadPriority Priority - vrací či nastavuje prioritu vlákna. Možné priority:
- AboveNormal, BelowNormal, Highest, Lowest, Normal
- ThreadState ThreadState - vrací stav vlákna. Možné stavy:
- Aborted, AbortRequested, Background, Running, Stopped, StopRequested, Suspended, SuspendRequest, Unstarted, WaitSleepJoin
methods
- void Start - spustí vlákno
- void Join - blokuje ostatní vlákna dokud neskončí aktuální vlákno
- void Abort - vyvolá výjimku ThreadAbortException signalizující, že by vlákno mělo být ukončeno
- void Interrupt - vyvolá výjimku ThreadInterruptedException jestliže je vlákno v blokovací stavu (ThreadState.WaitJoinSleep). Jestliže vlákno není blokováno, přerušení nikdy nenastane
- void Resume, Suspend - zastaralé, nedoporučuje se používat
Po trošcích teorie se pojdmě podívat na kousek kódu reprezentující spuštění kódu v novém vláknu. Třída Thread přijímá jako parametr objekt typu ThreadStart nebo ParameterizedThreadStart, který slouží k předání parametru novému vláknu, ale o tom později.
static void Main(string[] args)
{
// vytvorim objekt typu ThreadStart
ThreadStart vstupVlakna = new ThreadStart(NejakaMetoda);
// vytvorim nove vlakno
Thread vlakno = new Thread(vstupVlakna);
// spustime vlakno
vlakno.Start();
}
public static void NejakaMetoda()
{
Console.WriteLine("Metoda spustena v novem vlakne");
}
|
Při spuštění více vláken najednou je jejich použití zajímavější. Ukážeme si to na jednoduchém příkladě, kdy si spustíme několik vláken vykonávající ovšem stejný kód:
static void Main(string[] args)
{
// vytvorim objekt typu ThreadStart
ThreadStart vstupVlakna = new ThreadStart(Citac);
// vytvorim nova vlakno
Thread[] vlakna = new Thread[10];
for (int i = 0; i < vlakna.Length; i++)
{
// vytvorim nove vlakno
vlakna[i] = new Thread(vstupVlakna);
// a spustim
vlakna[i].Start();
}
}
public static void Citac()
{
Console.WriteLine("ID vlakna: {0}", Thread.CurrentThread.ManagedThreadId);
}
|
Pořadí vláken není přesně dáno a proto v případě, že chceme postupně provést několik vláken v konkrétním pořadí za sebou můžeme využít metodu Join, která nám zaručí, že se další vlákno spustí až skončí předchozí. V našem ukázkovém příkladu to je zbytečné, ale kdyby každé vlákno provádělo různě časově náročné operace, kód by vypadal takto
// projdeme kolekci vlaken
foreach (Thread t in vlakna)
{
// a zavolame prislusnou metodu
t.Join();
}
|
Nyní se vrátíme k objektu typu ParameterizedThreadStart o kterém jsem se zmínil výše. Vytvoření vlákna a jeho spuštění s parametrem by vypadalo následovně. Nesmíme ovšem zapomenout, že metoda, kterou definujeme při vytváření ParameterizedThreadStart objektu musí přijímat jako parametr objekt typu Object!
static void Main(string[] args)
{
// vytvorim objekt typu ParameterizedThreadStart
ParameterizedThreadStart paramVstupVlakna = new ParameterizedThreadStart(Citac);
// vytvorim nove vlakno
Thread vlakno = new Thread(paramVstupVlakna);
// spustim vlakno s parametrem
vlakno.Start("Muj parametr");
}
public static void Citac(object vstup)
{
Console.WriteLine("ID vlakna: {0}, parametr: {1}",
Thread.CurrentThread.ManagedThreadId, vstup);
}
|
Sdílení dat mezi více vlákny
Nejdříve si ukážeme kód a poté se podíváme na chyby a jejich řešení:
public static int cislo;
static void Main(string[] args)
{
ThreadStart start = new ThreadStart(Inkrementuj);
Thread[] vlakna = new Thread[10];
// spustim 10 vlaken, ktere budou provadet kod
// v metode Inkrementuj
for (int i = 0; i < 10; i++)
{
vlakna[i] = new Thread(start);
vlakna[i].Start();
}
// pockam az vsechny vlakna skonci
foreach (Thread t in vlakna)
{
t.Join();
}
// zobrazim vysledek
Console.WriteLine("Celkovy pocet: {0}", Program.cislo);
Console.ReadLine();
}
public static void Inkrementuj()
{
for (int i = 1; i <= 10000; i++)
{
Program.cislo = Program.cislo + 1;
}
}
|
Máme 10 vláken, které vykonávají stejnou operaci, tzn. , že každé vlákno provádí kód v metodě inkrementuj. Hodnota čísla se zvětší o 10 000, celkeme tedy 10 x 10 000 = 100 000. Pokud tento kód spustíme na jednojádrovém procesoru, tak výsledek by opravdu měl být 100 000, ovšem spustíme-li kód na vícejádrovém procesoru, dostaneme výsledek menší (ne vždy) než 100 000 ! Proč?
Inkrementace proměnné cislo se na jednojádrovém procesoru provádí ve třech krocích
- načtení hodnoty z paměti
- inkrementace hodnoty
- zapsání hodnoty zpět do paměti
u dvoujádrového
- první proces přečte hodnotu z paměti
- a inkrementuje
předtím, než ovšem dojde k zápisu zpět do paměti, je proces pozastaven a druhý proces spuštěn
- druhý proces přečte hodnotu z paměti
- inkrementuje
- a zapíše novou hodnotu zpět do paměti
druhý proces je pozastaven a první opět povolen
- první proces zapíše nyní špatnou hodnotu zpět do paměti
Jelikož náš kód není nijak ošetřen, tak během těchto operací dochází ke ztrátám některých hodnot, proto naše cislo není vždy 100 000. Jak to tedy ošetříme?
Využijeme třídu Interlocked, která nabízí statickou metodu Increment pro inkrementování proměnné. V metodě Inkrementuj uděláme menší změny
//Program.cislo = Program.cislo + 1;
Interlocked.Increment(ref Program.cislo);
|
tím zaručíme, že inkrementace bude správná a číslo tedy nebude menší než 100 000. Další možností je využít tzv. zámek (Lock), kterým si danou část uzamkneme a nikdo nám tam nevstoupí
public int cislo;
static void Main(string[] args)
{
Program p = new Program();
ThreadStart start = new ThreadStart(p.Inkrementuj);
Thread[] vlakna = new Thread[10];
// spustim 10 vlaken, ktere budou provadet kod
// v metode Inkrementuj
for (int i = 0; i < 10; i++)
{
vlakna[i] = new Thread(start);
vlakna[i].Start();
}
// pockam az vsechny vlakna skonci
foreach (Thread t in vlakna)
{
t.Join();
}
// zobrazim vysledek
Console.WriteLine("Celkovy pocet: {0}", p.cislo);
Console.ReadLine();
}
public void Inkrementuj()
{
lock (this)
{
for (int i = 1; i <= 10000; i++)
{
cislo = cislo + 1;
}
}
}
|
nebo případně využít třídu Monitor
public void Inkrementuj()
{
// vytvorime zamek
Monitor.Enter(this);
try
{
for (int i = 1; i <= 10000; i++)
{
cislo = cislo + 1;
}
}
finally
{
// uvolnime zamek
Monitor.Exit(this);
}
}
|
Při používání zámků si musíme dát pozor na tzv. Deadlocks
class Deadlocker
{
object ResourceA = new object();
object ResourceB = new object();
public void First()
{
lock (ResourceA)
{
lock (ResourceB)
{
// nejaky kod
}
}
}
public void Second()
{
lock (ResourceB)
{
lock (ResourceA)
{
// nejaky kod
}
}
}
}
|
V metodě main vytvoříme jedno vlákno, které bude provádět metodu First a druhé vlákno provádějící metodu second a spustíme
Deadlocker deadlock = new Deadlocker();
ThreadStart firstStart = new ThreadStart(deadlock.First);
ThreadStart secondStart = new ThreadStart(deadlock.Second);
Thread first = new Thread(firstStart);
Thread second = new Thread(secondStart);
first.Start();
second.Start();
first.Join();
second.Join();
|
dostaneme se do situace, že
- první vlákno uzamkne ResourceA
- druhé vlákno uzamkne ResourceB
- první vlákno čeká na uvolnění zámku ResourceB
- druhé vlákno čeká na uvolnění zámku resourceA
tyto 4 kroky nám signalizují "deadlock".
Mezi další synchronizační metody patří využití
- ReaderWriterLock třídy
- synchronizace pomocí windows objektů
- Mutex
- Semaphore
- AutoresetEvent
- ManualResetEvent
Asynchronní programování
Opět si představme, že v našem programu načítáme data ze souboru, ale jejich načítání trvá docelá dlouho. Proto abychom se zbavili odporných "hodin", můžeme načítání ze souboru provádět asynchronně a tudíž naše aplikace "nezatuhne". Ukážeme si to na příkladě
using (FileStream fs = new FileStream("S:\\test.txt", FileMode.Open,
FileAccess.Read, FileShare.Read,
1024, true))
{
byte[] buffer = new byte[fs.Length];
IAsyncResult result = fs.BeginRead(buffer, 0, buffer.Length, null, null);
while (!result.IsCompleted)
{
Console.WriteLine("ctu...");
}
int bytes = fs.EndRead(result);
Console.WriteLine("Precteno {0} byte-u", bytes);
Console.ReadLine();
}
|
Metodou BeginRead začneme číst data asynchronně. Pokud víme že čtení bude trvat déle, můžeme v cyklu while provádět další operace. A metoda EndRead nám vrátí počet přečtených byte-ů. Je možné si také vytvořit delegáta, která bude vykonán, až asynchronní čtení skončí
static void Main(string[] args)
{
FileStream fs = new FileStream("S:\\test.txt", FileMode.Open,
FileAccess.Read, FileShare.Read,
1024, true);
byte[] buffer = new byte[fs.Length];
IAsyncResult result = fs.BeginRead(buffer, 0, buffer.Length,
new AsyncCallback(CteniDokonceno), fs);
Console.ReadLine();
}
public static void CteniDokonceno(IAsyncResult result)
{
Console.WriteLine("Cteni dokonceno");
using (FileStream fs = (result.AsyncState as FileStream))
{
int bytu = fs.EndRead(result);
Console.WriteLine("Precteno {0} byte-u", bytu);
Console.ReadLine();
}
}
|
Nyní jsme metodě BeginRead předali jako parametr delegáta a filestream. Jakmile bude čtení dokončeno, provede se metoda CteniDokonceno
To je z dnešního krátkého souhrnu o vláknech vše. Naviděnou u dalšího dílu, který bude věnovaný nástrojům pro logování, debuggování, trasování a práci s WMI.
Lukáš