Vlákna v C# - 11. díl
 x   TIP: Přetáhni ikonu na hlavní panel pro připnutí webu

Vlákna v C# - 11. dílVlákna v C# - 11. díl

 

Vlákna v C# - 11. díl

Google       Google       5. 12. 2008       38 676×

V následujících dvou dílech nás čekají synchronizační konstrukce Wait a Pulse. V prvním z nich se podíváme, k čemu metody Wait a Pulse vůbec jsou a jak je použít.

Látka těchto dvou dílů je dlouhá a prolíná se, proto bude rozdělena do dvou částí. V první části se podíváme, k čemu Wait a Pulse vůbec jsou a jak je použít.

V pátém dílu jsme probrali třídy odvozené od abstraktní třídy WaitHandle – jednoduché signalizační konstrukce, kde se vlákno zablokuje, než dostane impulz od jiného vlákna.

Mnohem mocnější signalizační konstrukce nám poskytuje třída Monitor pomocí dvou statických metod. Jak už možná tušíte, jsou to metody Wait a Pulse. Ve zkratce to vypadá tak, že si celou signalizační logiku můžeme napsat sami, můžeme okopírovat funkcionalitu tříd AutoResetEvent, Semaphore, … a ještě si další věci přidat.

Největším problémem Wait a Pulse je jejich chabá dokumentace, částečně určitě způsobený tím, že využití nenajdou tak moc často. Abychom to neměli příliš jednoduché, tyto metody dokážou v kódu způsobit hotový chaos, pokud nevíme přesně, co děláme. Naštěstí existuje doporučený model použití, a pokud se jím řídíme, žádné chyby nám nehrozí. Použití Wait a Pulse a samotný model probereme v posledních dvou kapitolách tohoto dílu.

Úvod k Wait a Pulse

Účelem metod Wait a Pulse je poskytnutí jednoduchého signalizačního mechanismu: Wait zablokuje vlákno, dokud nedostane z jiného vlákna signál od metody Pulse.

Je logické, že Wait musí být zavoláno před Pulse (jinak by nebylo komu co signalizovat). Pokud ale nějakým nedopatřením přesto dojde k zavolání Pulse dříve, vůbec nic se nestane. Tady si můžete všimnout rozdílu oproti AutoResetEventu, u kterého se efekt odloží, pokud dojde k zavolání metody Set před WaitOne.

Když chceme v naší aplikaci použít Wait/Pulse, musíme definovat synchronizační objekt. Princip je jednoduchý – platí, že pokud obě vlákna používají stejný synchronizační objekt, můžou si mezi sebou posílat signály pomocí Wait a Pulse. Druhou důležitou věcí je, že synchronizační objekt musí být vždy uzamknut v locku, než ho použijeme při volání Wait nebo Pulse.

Co jsme si teď pověděli si ukážeme na příkladu:

class Test
{
    // Synchronizační objekt 'x'
    // Jako sync. obj. poslouží cokoliv
    // referenčního typu
    object x = new object();
}

Tento kód zablokuje vlákno „A“:

lock (x) Monitor.Wait (x);

A při zavolání tohoto kódu z vlákna „B“ dojde k odblokování vlákna „A“:

lock (x) Monitor.Pulse (x);

Jak to funguje?

Dokud vlákno čeká, metoda Monitor.Wait dočasně uvolňuje zámek kolem objektu „x“, aby ho mohlo jiné vlákno (to, které volá Monitor.Pulse) zase uzamknout. Celý proces můžeme vlastně napsat takto:

Monitor.Exit (x);             // Odemknutí zámku
// čekání na Pulse…
Monitor.Enter (x);            // Uzamknutí zámku

Proto se může Wait zablokovat ve skutečnosti dvakrát: poprvé, když čeká na Pulse a pak když se čeká na znovu-uzamknutí zámku. To také znamená, že Pulse neodblokuje čekající vlákno úplně, jen jakmile vlákno, které zavolalo Pulse, opustí kód v bloku lock může čekající vlákno skutečně začít pracovat.

Principu výše se říká lock toggling. Lock toggling nijak nezávisí na úrovni vnoření jednotlivých locků. Pokud třeba zavoláme Wait uvnitř dvou vložených locků:

lock (x)
    lock (x)
        Monitor.Wait (x);

Znovu si můžeme představit přepsání na Monitor.Exit a Monitor.Enter:

Monitor.Exit (x); Monitor.Exit (x);    // 2 locky = 2 Exit
// čekání na Pulse…
Monitor.Enter (x); Monitor.Enter (x);

Jak už víme z dřívějších dílů, jen první zavolání Monitor.Enter může způsobit zablokování.

Proč musíme lockovat?

Abych upřesnil nadpis: proč jsou vůbec metody Wait a Pulse vytvořeny tak, aby fungovaly jen uvnitř locku? Prvním důvodem je samozřejmě to, aby nehrozilo narušení thread-safety. Řekněme, že chceme zavolat Wait jen pokud je proměnná dostupny nastavená na false.

lock (x) 
{
    if (!dostupny) Monitor.Wait (x);
    dostupny = false;
}

Několik vláken najednou může spustit tento kód, ale žádné se nemůže přerušit mezi kontrolováním hodnoty dostupny a voláním Monitor.Wait (ano, je to atomické). Odpovídající operace s Pulse by vypadala takto:

lock (x)
{
    if (!dostupny) 
    {
        dostupny = true;
        Monitor.Pulse (x);
    }
}

Nastavení timeoutu

Timeout můžeme nastavit při volání Wait buďto klasicky v milisekundách nebo jako TimeSpan. Metoda Wait pak vrátí false, pokud vypršel timeout dřív, než se stihla zavolat Pulse. Tento časový limit ovlivní pouze čekací fázi, pokud ale vyprší, čekání bude probíhat stále! Příklad:

lock (x) 
{
    if (!Monitor.Wait (x, TimeSpan.FromSeconds (10)))
        Console.WriteLine ("Čas vypršel!");
    Console.WriteLine ("Ale zámek na x je pořád.");
}

Vlastnosti a nevýhody Pulse

Důležitou vlastností metody Pulse je to, že je volána asynchronně, tedy se nemůže nijak zablokovat. Pokud nějaké jiné vlákno čeká na impulz, dostane ho. Pokud žádné vlákno nečeká, efekt je ignorován.

Pulse disponuje jen jednosměrnou komunikací – vlákno posílající Pulse odešle signál čekajícímu vláknu. Nic víc. Pulse nevrací žádnou hodnotu indikující, jestli došel impulz k cíli, nebo jestli byl ignorován. Navíc, i když impulz skutečně k cíli dorazí, nemáme žádnou záruku, že čekající vlákno hned znovu nastoupí do služby. Může nastat dlouhá prodleva, než se procesor k tomuto vláknu vůbec dostane. Kvůli všem těmto komplikacím (a dost možná i nedokonalostem) by bylo těžké zjistit, kdy bylo vlákno odblokováno, kdy začalo pracovat, … jinak než prostřednictvím nějakých pomocných proměnných, které si sami definujeme. Proto nikdy nespoléhejte jen na časové odezvy při použití Wait a Pulse, mohli byste narazit.

Fronty čekatelů a PulseAll

Zavolat Wait na jeden objekt může najednou víc než jen jedno vlákno, tím se vytvoří tzv. „fronta čekatelů“ (waiting queue).

Každé zavolání Pulse pak uvolní vlákno na začátku fronty (je to tedy typ FIFO – First In, First Out), které se přesune do fronty odpulzovaných vláken a tam čeká na znovuobdržení zámku, stejně jako na obrázku:

Ovšem pozor. Pořadí, které vlákna získají tímto seřazením do fronty většinou nehraje roli, protože v aplikacích využívajících Wait a Pulse se nejedná tak úplně o frontu, jako spíše o vanu plnou čekajících vláken, ze kterých pak zavolání Pulse jedno vybere a propustí.

Třída Monitor poskytuje také metodu PulseAll, která, jak už název napovídá, propustí všechna čekající vlákna ve frontě (tedy vaně, chcete-li) najednou. Propuštěná vlákna nezačnou pracovat nastejno, protože všechna čekají na uzamknutí stejného objektu, tedy se znovu vytvoří fronta. Pokud bychom se podívali na obrázek, tak metoda PulseAll přesune všechna vlákna z „fronty čekatelů“ do fronty „odpulzovaných vláken“, kde čekají na získání zámku.

Jak použít Pulse a Wait

Definujme si dvě pravidla:

  • Jediná dostupná synchronizační konstrukce je lock (tedy Monitor.Enter a Monitor.Exit).
  • Nejsou žádná omezení co se spinningu týče (viz 2. díl seriálu).

S těmito pravidly na vědomí si ukažme následující příklad: pracovní vlákno, které čeká, dokud nedostane signál od hlavního vlákna.

class SimpleWaitPulse
{
    bool go;
    object locker = new object();

    void Work()
    {
        Console.Write("Čekám... ");
        lock (locker)
        {
            while (!go)
            {
                // Uvolníme zámek, takže hlavní vlákno bude moct
                // změnit hodnotu 'go'
                Monitor.Exit(locker);
                // Znovu uzamkneme zámek, abychom mohli znovu
                // otestovat hodnotu 'go' na začátku cyklu
                Monitor.Enter(locker);
            }
        }
        Console.WriteLine("Dostal jsem signál!");
    }

    void Notify() // Voláno z hlavního vlákna
    {
        lock (locker)
        {
            Console.Write("Signalizuji... ");
            go = true;
        }
    }
}
Abychom mohli kód spustit, potřebujeme ještě metodu Main:
static void Main()
{
    SimpleWaitPulse test = new SimpleWaitPulse();

    // Spustíme metodu Work na samostatném vlákně
    new Thread(test.Work).Start();  // "Čekám..."

    // Za vteřinu pozastavíme, pak odešleme signál:
    Thread.Sleep(1000);
    test.Notify();  // "Signalizuji... Dostal jsem signál!"
}

V metodě Work dochází ke spinningu - úplně zbytečně spotřebováváme procesorový čas tím, že neustále opakujeme obsah while cyklu, dokud nemá proměnná go hodnotu true. V tomto cyklu musíme odemykat a znovu uzamykat zámek, aby metoda Notify mohla uzavřít svůj lock a změnit hodnotu go. go je sdílená uvnitř celé třídy, proto k ní musíme přistupovat uvnitř zámku, abychom měli jistotu, že se její hodnota nezmění někde „mezi“ (nemůžeme použít volatile, jak jsme si řekli na začátku téhle kapitoly).

Zkuste kód uvedený výše zkompilovat, dostanete text: „Čekám… (vteřinová pauza) Signalizuji… Dostal jsem signál!“.

Teď pojďme náš kód upravit tak, aby namísto Monitor.Exit a Enter používal metody Wait a Pulse (výstup do konzole je vynechán naschvál, ať je to stručné):

class SimpleWaitPulse
{
    bool go;
    object locker = new object();

    void Work()
    {
        lock (locker)
            while (!go) Monitor.Wait(locker);
    }

    void Notify()
    {
        lock (locker)
        {
            go = true;
            Monitor.Pulse(locker);
        }
    }
}

Kód se chová stejně jako předtím, jen s jedním důležitým rozdílem – nedochází k žádnému spinningu. Metoda Wait dělá to samé co Monitor.Exit a Monitor.Enter, ale s žádným mezikrokem – když se zámek otevře, čekáme na zavolání Pulse. O to se postará metoda Notify, jakmile nastaví go na true. To je vše.

Model použití Pulse a Wait

Na začátku dnešního dílu jsme si řekli, že použití Pulse a Wait se může rychle zvrhnout ve velmi složité bludiště. V této kapitole ho prozkoumáme a ukážeme si ho na kousku pseudo-kódu. Tento vzor následoval už příklad v předchozí kapitole a silně doporučuji se ho držet i ve vašich aplikacích.

V kódu v předchozí kapitole jsme měli jen jednu proměnnou, které se týkalo zamykání (go). V jiné situaci bychom takových mohli potřebovat víc, postup je ale stejný jako s jednou proměnnou. Nejdřív si vzor ukážeme s použitím spinningu:

class X 
{
    //Blokované proměnné: jeden nebo více objektů zahrnutých v blokování, např:
    //bool go; bool ready; int semaphoreCount;...
    
    // locker chrání proměnné uvedené výše
    object locker = new object();
 
    void MetodaNaSpinning()
    {
        //... když chci vlákno zablokovat na základě proměnných
        lock (locker)
        {
            while (!) // Seznam podmínek podle gusta 
            {
                // Dáme ostatním vláknům šanci k upravení proměnných
                Monitor.Exit(locker);
                Monitor.Enter(locker);
            }
        }
    }

    void MetodaNaUpravu()
    {
        //... když chci upravit proměnné
        lock (locker)
        {
            //tady pracujte s proměnnými
        }
    }
}

Pokud opět spinning nahradíme metodami Pulse a Wait:

  • Exit a Enter nahradíme metodou Wait.
  • Když upravíme hodnoty proměnných, zavoláme Pulse těsně před uvolněním zámku.

Pseudo-kód bude vypadat takto:

class X
{
    //< Blokované proměnné ... >
    object locker = new object();

    void WaitMetoda()
    {
        //...
        //... když chci vlákno zablokovat na základě proměnných
        lock (locker)
        {
            while (!) // Seznam podmínek podle gusta 
            {
                Monitor.Wait(locker);
            }
        }
    }


    void PulseMetoda()
    {
        //... když chci upravit proměnné
        lock (locker)
        {
            //tady pracujte s proměnnými

            Monitor.Pulse(locker);
        }
    }
}

Ukázali jsme si bezpečný vzor použití Wait a Pulse, jeho hlavní výhody by se daly shrnout do několika bodů:

  • Blokovací podmínky jsou v podobě námi definovaných proměnných (které jsou logicky schopné fungovat i bez Wait a Pulse i při spinningu).
  • Wait je volána vždy uvnitř while cyklu, kde kontroluje blokovací podmínky (a cyklus sám je uvnitř locku).
  • Jeden jediný synchronizační objekt (v příkladu jsme použili locker) je použit pro všechna Wait a Pulse a ochraňuje tak všechny blokovací podmínky najednou.
  • Locky jsou tam jen kvůli nutnosti, ale vždy můžeme bez problémů zámek opustit.

Co je možná nejdůležitější - pokud budeme následovat tento vzor, zavolání Pulse nenutí čekatele, aby pokračoval v práci. Místo toho mu jen oznámí, že se „něco stalo“ a že by měl znovu zkontrolovat platnost blokovacích podmínek. Čekatel pak sám usoudí (dalším průchodem cyklu), jestli by měl znovu čekat, nebo cyklus opustit a pokračovat v práci. Výhoda je, že můžeme používat složité blokovací podmínky bez nějaké složité synchronizace.

Další výhodou je odolnost proti špatně zavolanému Pulse. To se stane, pokud se Pulse zavolá před Wait. Ale protože v tomto vzoru zavolání Pulse znamená „zkontroluj blokovací podmínky“ a ne „okamžitě pokračuj v práci“, může být příliš brzké zavolání Pulse ignorováno, protože než se zavolá samotné Wait, vždy se zkontroluje i blokovací podmínka.

Poslední poznámku mám k synchronizačnímu objektu. Díky tomu, že je jen jeden, můžeme přistupovat k proměnným atomicky. Pokud bychom měli pro lock, Pulse a Wait jiný synchronizační objekt, mohlo by docházet k deadlockům. Doporučuje se také deklarovat sync. objekt a jeho proměnné jen v rozsahu, kde je budeme skutečně potřebovat (tohoto omezení dosáhneme např. pomocí modifikátoru private).

Dnes jsme nakousli užitečnou problematiku světa Wait a Pulse a ukázali si, že pokud se používá podle pravidel, nic složitého to není. Příště náš průzkum dokončíme v přibližně stejně dlouhém článku.

Zdroj: http://www.albahari.com/threading/part4.aspx#_Wait_and_Pulse

×Odeslání článku na tvůj Kindle

Zadej svůj Kindle e-mail a my ti pošleme článek na tvůj Kindle.
Musíš mít povolený příjem obsahu do svého Kindle z naší e-mailové adresy kindle@programujte.com.

E-mailová adresa (např. novak@kindle.com):

TIP: Pokud chceš dostávat naše články každé ráno do svého Kindle, koukni do sekce Články do Kindle.

Hlasování bylo ukončeno    
0 hlasů
Google
Jakub studuje informatiku na FIT ČVUT, jeho oblíbenou platformou je .NET.
Web     Twitter     Facebook     LinkedIn    

Nové články

Obrázek ke článku Hybridní inteligentní systémy 2

Hybridní inteligentní systémy 2

V technické praxi využíváme často kombinaci různých disciplín umělé inteligence a klasických výpočtů. Takovým systémům říkáme hybridní systémy. V tomto článku se zmíním o určitém typu hybridního systému, který je užitečný ve velmi složitých výrobních procesech.

Obrázek ke článku Jak vést kvalitně tým v IT oboru: Naprogramujte si ty správné manažerské kvality

Jak vést kvalitně tým v IT oboru: Naprogramujte si ty správné manažerské kvality

Vedení týmu v oboru informačních technologií se nijak zvlášť neliší od jiných oborů. Přesto však IT manažeři čelí výzvě v podobě velmi rychlého rozvoje a tím i rostoucími nároky na své lidi. Udržet pozornost, motivaci a efektivitu týmu vyžaduje opravdu pevné manažerské základy a zároveň otevřenost a flexibilitu pro stále nové výzvy.

Obrázek ke článku Síla týmů se na home office může vytrácet. Odborníci radí, jak z pracovních omezení vytěžit maximum

Síla týmů se na home office může vytrácet. Odborníci radí, jak z pracovních omezení vytěžit maximum

Za poslední rok se podoba práce zaměstnanců změnila k nepoznání. Především plošné zavedení home office, které mělo být zpočátku jen dočasným opatřením, je pro mnohé už více než rok každodenní realitou. Co ale dělat, když se při práci z domova ztrácí motivace, zaměstnanci přestávají komunikovat a dříve fungující tým se rozpadá na skupinu solitérů? Odborníci na personalistiku dali dohromady několik rad, jak udržet tým v chodu, i když pracovní podmínky nejsou ideální.

Hostujeme u Českého hostingu       ISSN 1801-1586       ⇡ Nahoru Webtea.cz logo © 20032024 Programujte.com
Zasadilo a pěstuje Webtea.cz, šéfredaktor Lukáš Churý