Základ C# 21 – Vlákna [Threads]

Vlákna vám ponúkajú spôsob ako v programe robiť dve a viac vecí súčasne.

Vlákna sú úžasná vec dovoľujúca beh dvoch a viacerých metód zároveň.

Na čo sú a ako skutočne vlákna fungujú

Hlavné využitie vlákien je pri grafických programoch – vďaka tomu môže kód programu bežať a grafický program sa bude neustále aktualizovať. Druhé využitie je pokiaľ máte zložitejší kód, ktorý chcete rozdeliť na viac častí, ktoré spolu nesúvisia, čím trochu uľahčite procesoru. Ako som už napísal vlákna umožňujú beh dvoch a viacerých metód naraz. To však nieje tak úplne pravda – v skutočnosti sú len rozumne striedané inštrukcie rôznych vlákien a vyzerá to tak ako keby bežali naraz.

Ako vytvoriť vlákno

Tvorba vlákna je pomerne jednoduchá záležitosť, ak to však má byť spravené kvalitne, budete sa musieť zamyslieť, to je však už len na vás. Základná tvorba vlákna je vlastne napísanie akejkoľvek metódy. Túto metódu potom môžete samozrejme použiť aj klasickým spôsobom. Čo sa týka parametrov funkcie, musí to byť buď jeden parameter typu object, alebo žiadný.

public void Metoda()
{
   for (int i = 0; i < 1000000000; i++)
   {
       //Sťahovanie alebo niečo...
   }
}

Táto metóda by zastavila funkčnosť programu na poriadne dlho, pretože by čakal kým sa nedokončí. Pokiaľ by ste použili vlákno, toto sa nestane.

public void Main()
{
   Thread thread = new Thread(new ThreadStart(Metoda));
   thread.Start();
   Console.WriteLine("Done");
}

Na vytvorenie vlákna stačí vytvoriť nový objekt typu Thread, ktorý má parameter typu ThreadStart. Jeho parametrom je pre zmenu metóda návratového typu void bez parametrov. Pomocou metódy Start() môžete vlákno spustiť. zistíte, že vlákno beží, pritom nápís Done bol už napísaný. Pokiaľ by ste chceli na vaše vlákno počkať, môžete použiť metódu Join(). Aby ste mohli použiť triedu Thread, musíte mať použiť using System.Threading; .

public void Main()
{
   Thread thread = new Thread(new ThreadStart(Metoda));
   thread.Start();
   Console.WriteLine("Začiatok vlákna");
   thread.Join();
   Console.WriteLine("Koniec vlákna");
}

Nové vlákno by začalo pracovať po spustení metódou Start, základné vlákno by napísalo Začiatok vlákna a následne by sa vaše nové vlákno spojilo s pôvodným a to by čakalo kým sa Metoda nedokončí.

Vlákna s parametrami

Často budete chcieť metóde vlákna odoslať nejaké informácie – na to slúži trieda ParameterizedThreadStart, čo je alternatívny parameter pri tvorbe vlákna, tá ma ako parameter metódu s návratovým typom void a parametrom object. Parameter triede pošlete cez metódu Start, ktorej tento krát ako parameter pošlete objekt.

public void Metoda(object maximum)
{
   for (int i = 0; i < (int)maximum; i++)
   {
       //Sťahovanie alebo niečo...
   }
}

public void Main()
{
   Thread thread = new Thread(new ParameterizedThreadStart(Metoda));
   object parameter = 50010000;
   thread.Start(parameter);
   Console.WriteLine("Začiatok vlákna");
   thread.Join();
   Console.WriteLine("Koniec vlákna");
}

V tomto príklade vidíte, že metóda vlákna dostane ako parameter číslo 50010000, ktoré je uložené ako typ object a pretypovaný späť na typ int.

Asynchrónne metódy

Niektoré metódy ktoré v programoch použijete môžu byť označené modifikátorom async, tieto metódy môžu bežať zároveň s iným kódom akoby sa jednalo o iné vlákno.
Na ich vysvetlenie si môžeme ukázať jednoduchý príklad bez použitia asynchrónnych metód.

static private readonly List<int> dataList = new();
static private readonly Random randomizer = new();
private const int totalCount = 100;

public static void Main()
{
    DoStuff();
}

static private bool TryAddToList()
{
    double randNum = randomizer.NextDouble();

    if (randNum >= 0.0002)
        return false;

    dataList.Add((int)(randNum * 500000));
    return true;
}

static private void AddToList(int count)
{
    for (int i = 0; i < count;)
    {
        i += TryAddToList() ? 1 : 0;
    }
}

static private void DrawStatus(ref int startIndex)
{
    int length = dataList.Count;
    for (int i = startIndex; i < length; i++)
    {
        Console.Write(dataList[i] + ";");
    }
    startIndex = length;
}

static private void ReadList()
{
    int start = 0;
    DrawStatus(ref start);
}

static private void DoStuff()
{
    AddToList(totalCount);
    ReadList();
}

Čo tento kód robí? Rozoberieme si to postupne. Od prvej metódy.
Metóda TryAddToList sa pokúsi pridať číslo medzi 0-100 do zoznamu. Šanca na pridanie tohto čísla do zoznamu je 0.02%, takže naozaj malá, tým simulujeme dlhšie trvajúcu prácu.
Druhá funkcia AddToList pridá takto od zoznamu určité množstvo čísiel, ktoré je definované na 100.
DrawStatus vypíše všetky čísla v zozname, od zadaného začiatočného indexu, a po dokončení tento index aktualizuje. Vďaka tomu, ak pridáme neskôr do zoznamu viac čísiel budeme ich môcť ľahko zobraziť.
ReadList je len skratka na toto vypísanie čísel, v synchrónnej verzií reálne len zavolá DrawStatus od 0 a vypíše naraz všetky čísla. Dôvod prečo ju tu máme je ten, aby sa ľahšie porovnávalo z asynchrónnou metódou.
DoStuff je naša hlavná synchrónna funkcia, ktorá najprv naplní zoznam pomocou AddToList a následne ho vypíše pomocou ReadList.

Čo tento kód spraví? Presne to čo by ste čakali – najprv vygeneruje všetkých 100 čísel, a následne ich všetky naraz vypíše. Čo ak by sme ale chceli, aby čísla zároveň vypisovalo, kým ich generuje.

Použijeme na to 3 asynchrónne metódy – AddToListAsync, ReadListAsync a DoStuffAsync. Podľa názvov si ľahko domyslíte, že sa bude jednať o upravené verzie troch metód zo zoznamu vyššie.

AddToListAsync – Pretvoriť AddToList na asynchrónnu metódu nie je ťažké. Prvé čo musíme spraviť, je upraviť deklaráciu metódy – pred návratový typ pridáme slovíčko „async“ a typ samotný zmeníme na Task. Typ Task je generický, a môžete mu teda priradiť reálny typ objektu, ktorý vaša metóda vráti, takže ak by ste napríklad mali metódu Image LoadBitmap(string path) tak v asynchronnej verzií by vyzerala async Task<Image> LoadBitmap(string path). Pokiaľ metóda nevracia žiadny výsledok, a synchrónne by teda bola „void„, v asynchronnej verzií sa použije nešpecifikovaná verzia typu Task.
Po úprave deklarácie metódy musíme tiež upraviť jej obsah, aby na nás C# nekričal varovanie, že je metóda vnútorne pracovať synchrónne, čo v tomto prípade aj chceme, lebo naše fiktívne dáta musia byť uložené v správnom poradí. to spravíme tak, že kód i += TryAddToList() ? 1 : 0, ktorý používala pôvodná metóda zabalíme ako novú úlohu (Task) pomocou anonymnej funkcie. Na dokončenie tejto úlohy počkáme slovíčkom await, ktorého úlohou je čakať na dokončenie asynchrónnej úlohy.
Na záver len po dokončení tejto metódy nastavíme zastavovaciu vlajku, aby sme vedeli, že je všetko dokončené.

//Metóda je nastavená ako asynchrónna a jej návratová hodnota je nešpecifikovaný generický typ "Task"
//Pretože reálne nevraciame žiadne údaje.
static private async Task AddToListAsync(int count)
{
    for (int i = 0; i < count;)
    {
        //Pôvodný kód zabalíme do anonymnej metódy pomocou () => {}
        //A použijeme ho ako parameter pre spustenie novej úlohy.
        //Táto úloha beží asynchrónne, takže počkáme na jej dokončenie pomocou await
        await Task.Run(() => { i += TryAddToList() ? 1 : 0; });
    }
    //Nastavíme informáciu, že generovanie je dokončené
    stopMarker = true;
}

ReadListAsync upravíme podobným spôsobom, ale vypisovanie stavu opakujeme dokým nie je generovanie dokončené.

static private async Task ReadListAsync()
{
    int start = 0;
    //Opakujeme, kým nieje generovanie dokončené
    do
    {
        //DrawStatus podobne zabalíme do anonymnej metódy pre Task.Run a čakáme na jeho dokončenie
        await Task.Run(() => DrawStatus(ref start));
    } while (!stopMarker);
}

DoStuffAsync – Tá najdôležitejšia metóda, jej úlohou je spustiť pridávanie údajov do zoznamu a zároveň ich vypisovanie.

static private async Task DoStuffAsync()
{
    //Spustíme asynchrónne pridávanie údajov do zoznamu.
    //Keďže nečakáme na výsledok tohoto a rovno pokračujeme vo vypisovaní údajov,
    //tak môžeme výsledok tejto metódy zahodiť.
    //Jeho zahodenie nie je nutné pre funkčnosť kódu, Visual studio si však bez toho myslí,
    //Že s výsledkom ešte chcéme pracovať, a že na neho nečakáme.
    _ = AddToListAsync(totalCount);
    //V tomto okamihu už beží pridávanie čísel, my však okrem toho spustíme aj jeho vypisovanie
    //pomocou asynchrónnej funkcie ReadListAsync. Na jej dokončenie však už počkáme, aby sa táto
    //metóda vypla až po dokončení všetkých podúloh.
    await ReadListAsync();
}

Ako túto hlavnú metódu zavoláme z Main? Teoreticky by stačilo klasicky zavolať DoStuffAsync(), keďže je však naša metóda Main synchrónna. nemôžeme ju donútiť, aby čakala na dokončenie našej asynchrónnej metódy pomocou slovíčka await a program by sa vypol skôr, ako by bol dokončený. Jeden spôsob riešenia je použitie asynchronného vstupného bodu Main, čo C# podporuje. Lepším riešením je však použiť metódu Wait, ktorú Task obsahuje.

public static void Main()
{
    //Uložíme si informácie o spúšťanej úlohe
    Task task = DoStuffAsync();
    //A pomocou metódy Wait počkáme na jej dokončenie
    task.Wait();
}

Ak by vaša metóda vracala nejaký objekt, napríklad by sťahovala súbor z internetu, tak je tento pod jej dokončení možné získať pomocou parametra Result.

 Týmto sme si uzavreli základy jazyka C#, no táto séria ešte nekončí a tak sa ešte musíme pozrieť na užitočné triedy, ktoré nám .NET poskytuje a nabudúce sa pozrieme na čítanie a zapisovanie do súboru.

2 názory na “Základ C# 21 – Vlákna [Threads]”

    1. Vyzerá to tak, že sa budem musieť pozrieť na históriu tejto stránky. Som si istý že som to písal a len to omylom zmazal pri edite, ale je možné, že som to zabudol dokončiť… Tiež som videl komentáre pri 7. diele, a áno, sú tam isté problémy. Úprimne by som rád všetky články na webe kompletne prepracoval, keďže už majú 7 rokov a nie som spokojný z ich kvalitou. Ďakujem za informácie.

Pridajte Komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *

Scroll to Top