A Python programozási nyelv

Párhuzamosság

Szintaxis

Több processzt és valamilyen IPC-t használó párhuzamos programot minden nyelven írhatunk. Python programokban használhatunk viszont szálakat is, melyek a különálló processzekkel szemben közös adatterülettel rendelkeznek. Emiatt a szálak használatánál szükség van egy olyan mechanizmusra, ami a közösen használt adatokat védi attól, hogy egyszerre több szál is módosítsa, vagy, hogy valaki olvassa, miközben módosítás történik. A Python lehetőséget ad ezeknek az adatok a védelmére lockokkal, szemaforokkal.

Szálak használatára ad lehetőséget például a thread modul. Ez a modul POSIX szálakat használó operációs rendszereken támogatott. A támogatott platformok között van a Windows NT/95, SGI IRIX, Solaris 2.x, Linux. Ez a modul minimális lehetőségeket ad a szálak kezelésére. Általában, ha egy programban a főszál abortál, akkor minden szál futása véget ér. Nem így SGI IRIX operációs rendszeren, ahol a szálak tovább élnek. Ezek miatt érdemes figyelni arra, hogy a szálak futnak-e, amikor a fő szál éppen kilép. A thread modul nem ad lehetőséget ennek lekérdezésére, ezt nekünk kell valamilyen módon megvalósítani.

Egy lehetséges megoldást mutat be a következő Python program. A program működését leírják a dokumentációs sztringek és a kommentek a programban:

#!/usr/bin/python
 
import thread
import random
from time import sleep
 
class ThreadControl:
        '''Ez az osztály felügyeli a szálakat. Minden szál hozzá regisztálja be
        magát, ebből a felügyelő tudni fogja, hogy hány szál fut még.
        A fő szál ezek után megvárhatja, míg minden szál befejeződik, és csak
        utána lép ki, esélyt adva ezzel arra, hogy minden szál tisztességesen
        befejezze a működését.
        '''
        threads_should_quit = 0
 
        counter = 0     # szál számláló
        # lock objektum a számláló thread-safe módosításához
        lock = thread.allocate_lock()
 
        def add(self):
                'Regisztrál egy új szálat. Nyilvántartás nincs, csak számolás.'
                self.lock.acquire()
                self.counter += 1
                self.lock.release()
 
        def remove(self):
                'Eggyel csökkenti a számolt szálak számát.'
                self.lock.acquire()
                self.counter -= 1
                self.lock.release()
 
        def anyThreadRunning(self):
                'Igazat ad vissza, ha van még futó szál, egyébként hamisat.'
                self.lock.acquire()
                count = self.counter > 0
                self.lock.release()
                return count
 
        def threadsShouldQuit(self, value=None):
                '''Ez a függvény visszaadja a threads_should_quit változó
                értékét, amivel a főprogram jelzi a szálaknak, hogy szeretné, ha
                befejeződnének. Ha van második paraméter megadva, akkor
                beállítja ezt az értéket.'''
                if not value is None:
                        self.threads_should_quit = value
                return self.threads_should_quit
 
def thread_control_function(control):
        '''Ez a függvény fogja a szál működését meghatározni. A szál addig fut,
        amíg ez a függvény fut. Minden szálban külön névtér tartozik ehhez a
        függvényhez, amit bizonyít az, hogy a rand objektum azonosítója
        mindhárom szálnál különböző.'''
        # a korrekt működés egyik feltétele a körültekintően működő szál
        # jelzés a felügyelőnek, hogy új szál indul
        control.add()
        # a szál belső azonosítójának kiírása
        print "%d: thread running" % thread.get_ident()
        # Random példány későbbi használatra, és a példány id-jának kiírása
        rand = random.Random()
        print "%d: random object's id is %d"\
                                % (thread.get_ident(), id(rand))
 
        # jelzésig nyugodtan futhat a szál
        while not control.threadsShouldQuit():
                sleep_time = rand.random() * 5 + 1
                print "%d: sleeping for %1.2f seconds"\
                                % (thread.get_ident(), sleep_time)
                # véletlen ideig tétlenek leszünk
                sleep(sleep_time)
 
        # a jelzés megérkezett, a szál befejeződik
        print "%d: THREAD EXITING (random object at %d)"\
                                % (thread.get_ident(), id(rand))
        # a felügyelő csökkentheti a szálak számát
        control.remove()
 
# MAIN
# ThreadControl példányosítás
thread_control = ThreadControl()
# 3 szál létrehozása
for i in range(3):
        # új szál indítása, a szál a thread_control_function függvényt fogja
        # futtatni, és a második paraméterben adott tuple tartalmazza a neki
        # átadandó paramétereket, pillanatnyilag egyedül a ThreadControl
        # példányt
        thread.start_new_thread(thread_control_function, (thread_control,))
 
# a szálak kapnak egy kis időt, hogy fussanak
sleep(10)
# jelezzük a szálaknak, hogy új műveletbe már ne kezdjenek bele
thread_control.threadsShouldQuit(1)
print "THREADS SHOULD QUIT!"
 
# türelmesen megvárjuk, míg minden szál befejezi a futását, 0.1
# másodpercenként leellenőrizve ezt
while thread_control.anyThreadRunning():
        sleep(0.1)

A program kimenete valami ilyesmi lesz:

1026: thread running
1026: random object's id is 135084972
1026: sleeping for 1.58 seconds
2051: thread running
2051: random object's id is 135310788
2051: sleeping for 1.69 seconds
3076: thread running
3076: random object's id is 135305820
3076: sleeping for 1.80 seconds
1026: sleeping for 3.23 seconds
2051: sleeping for 2.55 seconds
3076: sleeping for 1.87 seconds
3076: sleeping for 1.04 seconds
2051: sleeping for 2.19 seconds
3076: sleeping for 3.49 seconds
1026: sleeping for 3.34 seconds
2051: sleeping for 5.23 seconds
1026: sleeping for 1.97 seconds
3076: sleeping for 5.66 seconds
THREADS SHOULD QUIT!
1026: THREAD EXITING (random object at 135084972)
2051: THREAD EXITING (random object at 135310788)
3076: THREAD EXITING (random object at 135305820)

Szinkronizáció

A mutex modul is lehetőséget ad kölcsönös kizárás megvalósítására. Bár nem kizárólag többszálas programoknál használható, de ezekben a programokban különösen hasznos lehet. Ez a modul egy zárolási kérés esetén egy sorba gyűjti a végrehajtandó függvényeket. A modul lehetőséget ad arra is, hogy egy erőforrás zárolt voltát ellenőrizzük, és arra is, hogy az ellenőrzéssel egyidőben atomi művelettel zároljuk is az erőforrást. Ennek akkor van jelentősége, ha egy erőforrás zároltsága esetén más műveletet szeretnénk végezni, és később esetleg ismét megpróbálkozni a zárolással.

Másik lehetőség a threading modul használata. Ez a modul szálak egy magasabb szintű kezelését teszi lehetővé, mint a thread modul. Nincs szükség rá, hogy mi tartsuk nyilván a futó szálakat. Használható egyszerre mindkét modul is - de a gyakorlatban ezt inkább kerüljük. Ugyanis, ha nem a threading modulon keresztül hoztuk létre a szálakat, akkor a threading modul nyilvántartása nem fogja tartalmazni ezeket a szálakat, így ha például a currentThread() függvényt használva szeretnénk elkérni az aktuális szál objektumot, akkor egy ún. "dummy" szál objektumot hoz létre, ami csak korlátozott funkciókkal rendelkezik. Ezzel csak feleslegesen bonyolítanánk a kódot.

Ezen kívűl a modul sok hasznos osztályt definiál:

Ezzel a modullal például nem kell már arra figyelnünk arra, hogy van-e még futó szál, amikor a fő szál már kilépne. Minden szálban egy flag mondja meg, hogy a szál démoni-e (daemonic). A főszál terminálás előtt megvár minden nem démon szálat. A főszál nem démoni, minden további szál pedig a létrehozó száltól örökli ennek a flagnek a kezdeti értékét (ami később természetesen módosítható). Az előző példa threading modult használó változatában például nincs démoni szál, így minden további erőfeszítés nélkül megkapja az esélyt minden szál arra, hogy "kitakarítson maga körül" (cleanup).

#!/usr/bin/python
 
from threading import *
import random
from time import sleep
 
# öröklés a Thread osztályból
class MyThread(Thread):
        # konstruktor
        def __init__(self, event, name):
                Thread.__init__(self, name=name)
                self.event = event
                self.rand = random.Random()
 
        def run(self):
                # a szálak konstruktorban megadott nevét írjuk, nem azonosítókat
                print "%s: RUNNING" % self.getName()
 
                # a konstruktorban eltárolt Event objektum jelzi majd, ha
                # bekövetkezik a "be kell fejeződni felhívás" esemény :)
                while not event.isSet():
                       sleep_time = self.rand.random() * 5 + 1
                        print "%s: sleeping for %1.2f seconds"\
                                        % (self.getName(), sleep_time)
                        # véletlen ideig tétlenek leszünk
                        sleep(sleep_time)
 
                # a jelzés megérkezett, a szál befejeződik
                print "%s: THREAD EXITING" % self.getName()
 
# MAIN
# 3 szál létrehozása
event = Event()
for i in range(3):
        thread = MyThread(event, "Thread-" + str(i))
        thread.start()
 
# a szálak kapnak egy kis időt, hogy fussanak
sleep(10)
# jelezzük a szálaknak, hogy új műveletbe már ne kezdjenek bele
event.set()
print "THREADS SHOULD QUIT!"
 
# a threading modulnak köszönhetően, mivel egyik szál sem démoni (daemonic),
# ezért a főszál megvárja a többi szál terminálását

A program kimenete hasonló lesz ehhez:

Thread-0: RUNNING
Thread-0: sleeping for 3.67 seconds
Thread-1: RUNNING
Thread-1: sleeping for 3.67 seconds
Thread-2: RUNNING
Thread-2: sleeping for 3.67 seconds
Thread-1: sleeping for 1.14 seconds
Thread-0: sleeping for 1.14 seconds
Thread-2: sleeping for 1.14 seconds
Thread-0: sleeping for 1.82 seconds
Thread-1: sleeping for 1.82 seconds
Thread-2: sleeping for 1.82 seconds
Thread-1: sleeping for 3.12 seconds
Thread-0: sleeping for 3.12 seconds
Thread-2: sleeping for 3.12 seconds
Thread-0: sleeping for 4.19 seconds
Thread-1: sleeping for 4.19 seconds
Thread-2: sleeping for 4.19 seconds
THREADS SHOULD QUIT!
Thread-1: THREAD EXITING
Thread-0: THREAD EXITING
Thread-2: THREAD EXITING

Ezekben a példákban a standard outputra való írást nem kezeltük korlátozott erőforrásként, pedig az. Az egyszerűség kedvéért most vállaltuk a kockázatot, hogy a szálak egymás soraiba piszkítanak, egy körültekintő programnak azonban figyelnie kell erre is.

A Thread osztály metódusaival hasonló szálkezelést használhatunk, mint ami a Javaban is megtalálgató, noha számos onnan ismert funkciót nem támogat a Python, így például nincs lehetőség szálak felfüggesztésére és későbbi újraindítására, nem lehet egy szálat megállítani vagy megszakítani. Talán a legfontosabb, hogy míg Javaban nyelvi elem van a kölcsönös kizárás megvalósítására, addig Pythonban ezt különálló objektumokkal kell megvalósítani. Van lehetőségünk viszont arra, hogy egy szál join metódusát hívva a hívó szál futása blokkolódjon, amíg a hívott objektum által reprezentált szál futása be nem fejeződik. Ennek a hívásnak egy időkorlátos változata is van, ami az opcionális paraméterrel adható meg.