Hlavní navigace

Létající cirkus (13)

7. 6. 2002
Doba čtení: 7 minut

Sdílet

V dnešním dílu seriálu o jazyce Python se budeme věnovat vláknům a jejich synchronizaci. Probereme obě dvě rozhraní, která lze v Pythonu používat, a ukážeme si použití jednotlivých prostředků pro synchronizaci vláken.

Úvod

Vlákna jsou prostředkem známým z mnoha operačních systémů. Lze je chápat jako samostatné procesy. Jednotlivá vlákna běží nezávisle na sobě, přičemž ale sdílí společný adresový prostor. Změna jedné proměnné v jednom vlákně se ihned promítne do ostatních vláken. Někdy nám však tato nezávislost může být na překážku. Jedno vlákno může třeba chtít testovat určitou proměnnou na hodnotu „1“, proměnnou tedy načte a chce ji otestovat. Než to ale stihne, je přerušeno operačním sytémem a v běhu pokračuje jiné vlákno. To mezitím změní danou proměnnou na „0“. Pak je přerušeno a řízení se vrátí vláknu prvnímu. To má stále načtenu hodnotu „1“, přestože proměnná již má hodnotu „0“. Vlákna tudíž potřebují určité prostředky pro synchronizaci. Pomocí těchto prostředků lze zajistit, že k určitým prostředkům (proměnným, souborům atd.) bude mít přístup vždy pouze jedno vlákno.

Nejjednodušším synchronizačním prostředkem je zámek. Zámek má dva stavy (odemčený a zamčený) a umožňuje dvě operace (zamčení a odemčení samozřejme). Jejich funkci si vysvětlíme na následujícím příkladu. Mějme dvě vlákna A a B, sdílenou proměnnou X a zámek Z. Nyní chce A přistupovat k X. Zamkne proto Z a začne pracovat s proměnnou X. Mezitím k téže proměnné chce přistupovat i vlákno B. Rozhodne se tedy zamknout Z. Ten je však již zamčen vláknem A, vlákno B proto musí čekat, dokud ho A opět neodemkne. Mezitím vlákno A dokončilo operace s X a zámek Z odemklo. Odemčení Z umožnilo vláknu B ho znovu uzamknout. B tak učiní a nadále pracuje výhradně s X. Nakonec zámek Z odemkne a pokračuje v činnosti. Oblasti, která je obklopena operacemi uzamčení a odemčení, se říká „kritická“. Zámky zajišťují, že kritická oblast je prováděna vždy jen jedním vláknem a ostatní čekají, až tuto oblast opustí. S tím souvisí další problémy – musíme zabránit „dostihům“ a „uváznutí“ při používání více prostředků chráněných více zámky. Tato problematika je však již mimo rámec našeho seriálu. Více se o ní dozvíte z odborné literatury (například většina knih o programování zmiňující se o vláknech apod.).

Implementace vláken v Pythonu používá jeden zámek pro všechny interní proměnné interpretru, proto může vždy běžet pouze jediné vlákno interpretru. Na jednoprocesorových strojích je tento problém bezvýznamný (na jediném procesoru se může v jednom okamžiku vykonávat pouze jedna instrukce, tudíž jediné vlákno a jediný program), o to více je ale palčivější na víceprocesorových počítačích, které umožňují opravdový souběžný běh několika programů či vláken. Program v Pythonu používající vlákna pak běží v jednom okamžiku pouze na jednom procesoru a oproti jednoprocesorové konfiguraci není rychlejší ani o píď.

Modul thread

Tento modul nabízí nízkoúrovňová primitiva, pomocí nichž může programátor vytvářet nová vlákna a jednoduché zámky. Lze jej přikompilovat na všech systémech kompatibilních se standardem POSIX, dále pak ve Windows 95 a NT, SGI IRIX a Solaris 2.x.

Všechny funkce, které modul thread nabízí, jsou velmi nízkoúrovňové, proto bych vám doporučoval raději používat modul threading, který je vrstvou nad modulem thread a poskytuje opravdu komfortní rozhraní pro práci s vlákny.

Nejvýznamnější funkcí modulu thread je start_new_thread(). Té musíme jako první předat funkci, která se stane tělem vlákna. Pak musí následovat seznam pozičních argumentů jako tuple a nakonec lze uvést i nepovinné asociativní pole keyword argumentů. Každé vlákno má přiřazeno vlastní identifikační číslo. Toto číslo mají různá vlákna různé, ale po ukončení jednoho vlákna může být jeho číslo přiřazeno nějakému nově vzniklému vláknu. Hodnotu tohoto čísla získáme funkcí thread.get_ident(). Každé vlákno můžeme ukončit voláním buď funkce sys.exit(), nebo funkce thread.exit(). Stejný efekt má i neodchycená výjimka SystemExit.

Modul thread nám umožňuje vytvořit i ten nejjednodušší zámek. Slouží k tomu funkce thread.alloca­te_lock(). Ta je volána bez argumentů a vrátí objekt, který reprezentuje zámek. Zámek nám nabízí tři metody: acquire(), release() a locked(). Metoda acquire() zámek uzamkne. Pokud je zámek již uzamčen, čeká, dokud nebude odemčen jiným vláknem, a až poté ho uzamkne. Této metodě můžeme předat i počet vteřin, jak dlouho má maximálně čekat, pokud je zámek uzamknut. Uplyne-li tato doba, vrátí hodnotu false, jinak vrátí true. Metoda release() zámek odemkne, přičemž ho následně může uzamknout jiné vlákno čekající na jeho odemčení. Implementace zaručuje, že čeká-li na odemčení více než jedno vlákno, může ho znovu uzamknout vždy pouze jedno jediné.

Modul threading

Pomocí jednoduchého zámku lze implementovat i složitější synchronizační mechanismy jako reentrantní zámky, události, podmínky nebo semafory. Všechny tyto nástroje nám nabízí vysokoúrovňový modul threading, který je již celý napsaný v Pythonu a používá nízkoúrovňový céčkový modul thread. Všechny funkce tohoto modulu je možné importovat pomocí „from threading import *“.

Nejprve se podíváme na třídu Thread reprezentující vlákna. Od této třídy můžeme bez problémů odvodit svou vlastní třídu a předefinovat tak implicitní chování vlákna. To, jaký kód bude vlákno vykonávat, můžeme ovlivnit dvojím způsobem, buď předáním funkce konstruktoru třídy Thread, nebo předefinováním metody run() této třídy. V odvozené třídě byste měli přepsat pouze konstruktor __init__() a metodu run(), ostatní metody by měly zůstat beze změny:

>>> import threading

>>> import time
>>> def vlakno2():
...     for i in range(5):
...         print 'vlakno2'
...         time.sleep(0.2)
...
>>> def vlakno1():
...     for i in range(10):
...         print 'vlakno1'
...         time.sleep(0.1)
...
>>> v1 = threading.Thread(target = vlakno1)
>>> v2 = threading.Thread(target = vlakno2)
>>> v1.start(); v2.start()

Předchozí příklad byl jednoduchou ukázkou, jak vytvořit a spustit dvě vlákna. Objekty v1 a v2 reprezentují každé jedno vlákno. Objekt vlákna má také několik metod. Především je to metoda start(), která spustí tělo vlákna specifikované parametrem target jeho konstruktoru nebo metodou run(). Při spouštění vláken dávejte pozor na omyl, kdy chybně napíšete v1.run() a tělo se spustí v aktuálním (hlavním) vlákně místo vytvoření vlákna nového!

Objekty vláken mají ještě další užitečné metody. Především je to metoda join(). Je-li zavolána pro nějaké vlákno, pak vlákno, které ji spustilo, je zablokováno do doby, dokud vlákno, jehož join() bylo zavoláno, neskončí svou činnost. Další užitečná metoda je isAlive(), která vrátí true, pokud vlákno ještě běží.

Následující metoda setDaemon() přebírá logickou hodnotu. Je-li tato hodnota true, vlákno je klasifikováno jako démon. Program, používající vlákna, skončí, když svoji činnost dokončí všechna vlákna, která nejsou démoni. Démoni jsou pak automaticky ukončeni. Metoda isDaemon() vrací hodnotu příznaku nastavenou metodou setDaemon(), při vytvoření je vlákno klasifikováno jako normální („ne-démon“). Poslední dvojice metod getName() a setName() umožňuje jednotlivá vlákna pojmenovat. Vláknu můžeme přiřadit jméno i při jeho vytvoření předáním keyword argumentu ‚name‘ konstruktoru třídy Thread.

Modul threading obsahuje, kromě několika tříd, i některé funkce. Především jsou to activeCount(), která vrátí počet aktivních vláken, currentThread(), která vrací objekt aktuálního vlákna, a enumerate() vracející seznam všech aktivních vláken.

Synchronizační prostředky

Prvním synchronizačním prostředkem je zámek, jenž získáme voláním funkce Lock(), která je pouze odkazem na funkci thread.alloca­te_lock() (viz zdrojový kód modulu threading). Jednoduchým příkladem použití může být následující ukázka:

>>> import threading
>>> import time
>>> zamek = threading.Lock()

>>> def vlakno2():
...     zamek.acquire()
...     for i in range(5):
...         print 'vlakno2'
...         time.sleep(0.2)
...     zamek.release()
...
>>> def vlakno1():
...     zamek.acquire()
...     for i in range(10):
...         print 'vlakno1'
...         time.sleep(0.1)
...     zamek.release()
...
>>> v1 = threading.Thread(target = vlakno1)
>>> v2 = threading.Thread(target = vlakno2)
>>> v1.start(); v2.start

Mírná modifikace předchozího příkladu ukazuje použití zámku. Tato úprava zabrání pomíchání výstupu jednotlivých vláken. Kritickou oblastí v tomto případě jsou cykly pro výstup na obrazovku.

Další typy synchronizčních prostředků

Reentrantní zámek je modifikací nejjednoduššího zámku. Tento zámek může být jedním vláknem uzamčen (a tedy i odemknut) více než jednou. Pokud se ovšem uzamčený zámek pokusí uzamknout jiné vlákno, musí počkat, až bude úplně odemčen. Tento zámek získáme voláním funkce threading.RLock(), vrácený objekt podporuje stejné metody jako jednoduchý zámek – acquire() a release().

skoleni

Semafor je nejstarší synchronizační primitivum. Semafor reprezentuje určitou hodnotu. Každé uzamčení tuto hodnotu sníží o jedničku, odemčení hodnotu o jedničku zvýší, přičemž hodnota se nesmí dostat do záporných čísel. Pokusí-li se některé vlákno semafor uzamknout a jeho hodnota je rovna nule, je toto vlákno zablokováno a musí čekat, dokud nějaký zámek semafor neodemkne. Semafor vrací funkce threading.Semap­hore(), které předáme hodnotu určující počáteční nastavení semaforu. Metody jsou opět stejné jako u zámku. Semafor s počáteční hodnotou nastavenou nastavenou na 1 má téměř stejné chování jako zámek. Existuje jediný rozdíl: odemčení odemčeného zámku vyvolá výjimku, u semaforu pouze zvýší hodnotu o jedničku (tedy na hodnotu 2). Tomuto chování zabraňuje speciální případ semaforu – BoundedSemaphore, který vrací stejnojmenná funkce modulu threading. Ten pracuje stejně jako obyčejný semafor, má-li ovšem při volání metody release() dojít k překročení počáteční hodnoty, dojde k výjimce ValueError. Semafory zajišťují, že do kritické oblasti nevstoupí více než n vláken, kde n je počáteční hodnota semaforu. Bounded semafor dokáže navíc zabránit chybám, které vznikají vícenásobným odemčením semaforu. Na více nám v dnešním díle již nezbyl prostor. Proto pro popis dalších synchronizačních prvků a vlastností implementace vláken v jazyce Python nahlédněte, prosím, do dokumentace k modulům thread a threading.

Příště

V dalším dílu se podíváme na perzistentní objekty a na moduly pro práci s nimi – pickle, shelve, marshal.