Monitor (synchronizace)

Monitor je synchronizační primitivum, které se používá pro řízení přístupu ke sdíleným prostředkům. Jeho zvláštností je, že jde o speciální konstrukci programovacího jazyka (musí ho tedy implementovat překladač), typicky implementovanou pomocí jiného synchronizačního primitiva. Výhodou monitoru oproti jiným primitivům je jeho vysokoúrovňovost – snadněji se používá a je bezpečnější. Při jeho použití je méně pravděpodobné, že programátor udělá chybu.

Monitor se skládá z dat, ke kterým je potřeba řídit přístup, a množiny funkcí, které nad těmito daty operují.

Vzájemné vyloučení

Monitor se podobá třídě z OOP. Odlišností je to, že překladač doplní monitor o zámek, díky němuž se dosáhne vzájemného vyloučení – v jednu chvíli může být uvnitř monitoru jen jeden proces.

Když chce proces vstoupit do monitoru (tj. zavolat jeho funkci), musí nejdříve získat zámek. Pokud zámek v tu chvíli drží někdo jiný, tak se proces zablokuje a čeká, dokud se zámek neuvolní (tj. dokud jiný proces neopustí monitor nebo nezačne čekat na podmíněnou proměnnou).

Celý proces zamykání je pro programátora transparentní. V programu se funkce monitoru volají stejně jako ostatní funkce. Kód, který provádí zamykání a odemykání, vygeneruje překladač.

Monitor většinou splňuje dodatečné podmínky:

  • data monitoru jsou přístupná jen z jeho funkcí,
  • funkce monitoru nepoužívají data mimo monitor,
  • každá funkce zajistí, že před uvolněním zámku jsou data v konzistentním stavu.

Pokud jsou tyto podmínky splněny, platí, že žádný proces nenajde data v nekonzistentním stavu, což je přesně důvod pro zavedení synchronizačního primitiva.

Jako jednoduchý příklad poslouží monitor pro bankovní účet:

monitor účet {
  int zůstatek := 0
  
  function vybrat(int částka) {
    if částka < 0 then error "Vybíraná částka nesmí být záporná"
    else if zůstatek < částka then error "Nedostatečný zůstatek"
    else zůstatek := zůstatek - částka
  }
  
  function vložit(int částka) {
    if částka < 0 then error "Vkládaná částka nesmí být záporná"
    else zůstatek := zůstatek + částka
  }
}

Pokud by bankovní účet nebyl v monitoru, mohlo by dojít k souběhu (race condition): Řekněme, že na účtu je 200 Kč a dva procesy chtějí najednou každý vybrat 150 Kč. Bez synchronizace může dojít k tomu, že ověření zůstatku proběhne u obou procesů dříve, než jsou peníze odečteny. Každý proces tak vidí na účtu 200 Kč a povolí odečtení částky. Po skončení obou operací je ale účet v nekonzistentním stavu – obsahuje −100 Kč.

Díky monitoru ale jeden proces získá zámek dřív a druhý proces zatím musí čekat. První proces ověří zůstatek, odečte 150 Kč a vyskočí z monitoru. Až poté se do monitoru dostane druhý proces, který ohlásí nedostatek zůstatku na účtu.

Podmíněné proměnné

Občas je potřeba, aby proces, který je právě v monitoru, počkal na nějakou událost. Monitor poskytuje tuto funkcionalitu pomocí tzv. podmíněných proměnných.

Když funkce monitoru potřebuje počkat na splnění podmínky, vyvolá operaci wait na podmíněné proměnné, která je s touto podmínkou svázána. Tato operace proces zablokuje, zámek držený tímto procesem je uvolněn a proces je odstraněn ze seznamu běžících procesů a čeká, dokud není podmínka splněna. Jiné procesy zatím mohou vstoupit do monitoru (zámek byl uvolněn). Pokud je jiným procesem podmínka splněna, může funkce monitoru „signalizovat“, tj. probudit čekající proces pomocí operace notify.

„Zajímavých událostí“ může být více, každá může být spojená s vlastní podmíněnou proměnnou. Operace notify budí jen ty procesy, které provedly wait na stejné proměnné.

Následující monitor používá podmíněné proměnné k implementaci komunikačního kanálu, který v jednom okamžiku obsahuje jen jedno číslo.

monitor kanál {
  int obsah
  boolean naplněn := false
  condition posláno
  condition přijato

  function poslat(int data) {
    while naplněn then wait(přijato)
    obsah := data
    naplněn := true
    notify(posláno)
  }

  function přijmout() {
    var int data

    while not naplněn then wait(posláno)
    data := obsah
    naplněn := false
    notify(přijato)
    return data
  }
}

V dřívějších implementacích signalizování způsobilo, že čekající proces se okamžitě rozběhl a získal zámek (signalizující proces se zablokoval, dokud se vzbuzený proces zámku zase nevzdal), čímž bylo zaručeno, že podmínka je stále ještě splněna. Implementace tohoto chování je komplikovaná a má velkou režii. Je také nekompatibilní s plánovači, které mohou proces kdykoliv přeplánovat. Z těchto důvodů existuje několik různých sémantik podmíněných proměnných, co se týče signalizování. Ve většině moderních implementací signalizace neblokuje signalizující proces, pouze způsobí, že proces čekající na podmíněnou proměnou bude čekat na získání zámku (prakticky je proces přeřazen z jedné fronty do druhé).

Tento přístup má dva vedlejší efekty. Signalizující proces nemusí před signalizací uvést monitor do konzistentního stavu, protože stále drží zámek. Na druhou stranu nelze zaručit, že důvod signalizace stále platí ve chvíli, kdy se probuzený proces rozběhne. Mezitím totiž mohl jiný proces podmínku opět zneplatnit. Čekání na podmínku tedy musí být nezbytně implementováno pomocí dvojí kontroly – podmínka se testuje před i po volání wait a pokud není splněna, opět se volá wait. To se řeší použitím smyčky namísto jednoduchého podmíněného příkazu.

Většina implementací také poskytuje operaci notifyAll, která probudí všechny procesy čekající na danou podmíněnou proměnnou.

Navzdory svému názvu nenabývají podmíněné proměnné hodnot true a false. Vlastně nemají žádnou přístupnou hodnotu. Bývají to fronty procesů, které čekají na splnění podmínky, ale tato fronta je zvenčí nepřístupná, jediné, co lze s podmíněnými proměnnými provést, je volat na nich operace wait a notify. Samotná podmínka (např. zda sdílená datová struktura je plná/prázdná) musí být uchovávána v běžné proměnné.

Podpora v programovacích jazycích

V některých objektově-orientovaných programovacích jazycích jsou monitory hlavním synchronizačním primitivem kvůli své vysokoúrovňovosti: objekty mohou být přímo svázány s monitory, každý objekt automaticky vlastní svůj unikátní monitor.

Java

V programovacím jazyku Java jsou monitory hlavním synchronizačním primitivem. Každý objekt má automaticky přiřazen svůj monitor. Metody (funkce), které patří do monitoru, jsou označeny pomocí klíčového slova synchronized. Do monitoru libovolného objektu však lze obalit libovolný blok kódu pomocí konstrukce synchronized(objekt) { ... } (ve skutečnosti je označení celé metody tímto klíčovým slovem jen syntaktická zkratka pro tuto konstrukci použitou na aktuální objekt – this).

Java nepodporuje (alespoň ne přímo v jazyce) podmíněné proměnné. Operace wait, notify a notifyAll jsou implementovány jako metody třídy Object, která je společným předkem všech tříd. V podstatě tak každý objekt obsahuje právě jednu podmíněnou proměnnou, která udržuje seznam čekajících vláken.

Stejně jako zámek, který zajišťuje vzájemné vyloučení, je tato podmíněná proměnná pro programátora transparentní a nepřístupná.

Původně byly monitory jediným v Javě dostupným synchronizačním primitivem (ostatní primitiva ale bylo možné simulovat). Ve verzi 1.5 byl do jazyka přidán balíček java.util.concurrent, který obsahuje i jiná synchronizační primitiva.

Příklad

Implementace příkladu s komunikačním kanálem uvedeného výše v programovacím jazyce Java:

class Kanál {
   private int data;
   private boolean naplněn = false;
   
   public synchronized int přijmout() {
       while (!naplněn) {
           try {
               wait();
           } catch (InterruptedException e) {}
       }
       naplněn = false;
       return data;
   }
   
   public synchronized void poslat(int hodnota) {
       data = hodnota;
       naplněn = true;
       notify();
   }
}

.NET

V prostředí .NET (a tedy např. programovací jazyk C#) se používá prakticky stejný princip: každý objekt má svůj monitor, obdobou klíčového slova synchronized je v C# klíčové slovo lock. Navíc však .NET poskytuje i nízkoúrovňovější přístup k monitorům pomocí třídy Monitor a jejích metod (např. Enter a Exit), které dovolují o něco flexibilnější (ale „nebezpečnější“) řízení vzájemného vyloučení. Operace wait, notify a notifyAll jsou v .NETu podporovány taktéž prostřednictvím metod třídy Monitor (konkrétně Wait, Pulse, PulseAll).

.NET již od počátku podporuje i další synchronizační primitiva a další nástroje pro synchronizaci a paralelní programování.

Historie

Jako první popsal a implementoval monitory dánsko-americký počítačový vědec Per Brinch Hansen, přičemž vycházel z myšlenek C. A. R. Hoara. Ten pak následně vyvinul teoretický rámec a dokázal jejich ekvivalenci se semafory.

Odkazy

Související články

Externí odkazy