Obsah
1. Programovací jazyk Lua a koprogramy
2. Funkce umožňující práci s koprogramy
3. Stavy koprogramu
4. Přerušení koprogramu a obnova jeho běhu
5. Inicializace koprogramu pomocí parametrů
6. Předávání parametrů mezi koprogramem a volající funkcí
7. Zdrojové kódy všech demonstračních příkladů
8. Odkazy na Internetu
9. Obsah dalšího pokračování seriálu
1. Programovací jazyk Lua a koprogramy
V programovacím jazyku Lua je implementována podpora pro takzvané koprogramy (coroutines). Koprogram je možné chápat jako zobecnění podprogramu, procedury, funkce či metody. Volání běžných podprogramů a návrat z nich je řízen pomocí zásobníku (stack) – při každém volání podprogramu je adresa aktuálního místa, kde se výpočet nachází, uložena na zásobník a při ukončení podprogramu je tato adresa ze zásobníku vyjmuta a řízení se vrátí na (přesněji těsně za) místo, ve kterém byl podprogram, metoda či funkce volána. To znamená, že podprogram má jen jeden vstupní bod a při ukončení podprogramu se musí v případě potřeby podprogram opět zavolat od tohoto vstupního bodu (přerušený výpočet nelze po návratu z podprogramu dokončit), což odpovídá klasicky chápanému strukturovanému programování. Díky tomu, že jednou přerušený podprogram již není možné obnovit, může celá aplikace používat pouze jediný zásobník pro předávání parametrů i návratových hodnot (výjimkou jsou samozřejmě uzávěry, jejichž lokální proměnné nejsou ukládány na zásobník, ale na haldu – heap).
Naproti tomu v případě použití koprogramů je možné jednoduchým způsobem definovat libovolné množství vstupních bodů, které se mohou nacházet v jejich těle na prakticky libovolném místě (včetně vnitřních částí smyček). Činnost koprogramu lze v těchto bodech přerušit a vrátit řízení volajícímu programu, ovšem s tím, že se vnitřní stav koprogramu zachová a běh koprogramu tak lze od tohoto bodu znovu spustit (nespouští se tedy od svého začátku, ale od bodu, kde byl výpočet přerušen). Navíc je možné v těchto bodech předávat parametry jak z volajícího programu do koprogramu, tak i opačným směrem – toto předání parametrů je bezpečné (mohli bychom říci jazykem Javistů threadsafe, i když koprogramy nepoužívají vlákna operačního systému), v podstatě se jedná o jediný (a dostatečný) synchronizační mechanismus, který je při práci s koprogramy zapotřebí. Vzhledem k tomu, že je nutné uchovávat stav koprogramu i tehdy, když je jeho výpočet přerušen, musí mít koprogramy vlastní zásobník, který je v operační paměti alokován ve chvíli, kdy je koprogram vytvořen (o jeho dealokaci se postará garbage collector).
S využitím koprogramů lze velmi jednoduše implementovat kooperativní multithreading, ovšem s tím omezením, že se ve skutečnosti nepoužívají vlákna (thready) operačního systému, protože volající program je při zavolání koprogramu pozastaven, aby mohl počkat na výsledek jeho běhu. To na jednu stranu zjednodušuje implementaci vláken v Lua (vlákna jsou tak podporována na všech platformách, nezávisle na použitém operačním systému), na stranu druhou se nevyužívá všech možností moderních vícejádrových mikroprocesorů. V současné verzi jazyka Lua lze multithreading nabízený operačním systémem využít přes vlastní API vytvořené aplikací, která interpretr Lua obsahuje (tj. céčkový program má zaregistrovány funkce pro vytvoření vlákna, jeho zrušení atd.), ovšem veškerou synchronizaci mezi vlákny, například při předávání dat, je nutné provádět také přes toto API, typicky implementací semaforů, synchronizovaného asociativního pole apod. (při použití asociativního pole lze využít metametod navázaných na události __index a __newindex popsaných v předchozí části seriálu).
Také je možné použít opačný přístup – v céčkovém programu lze (od verze 5.1) vytvořit několik prostředí (environment) interpretru, přičemž každé prostředí může běžet v jiném vlákně či procesu. Jednou z pěkných a především snadno použitelných implementací „skutečného“ paralelního multithreadingu představuje projekt Lua Lanes, který navíc zavádí komunikaci mezi vlákny ve stylu jazyka Linda – viz http://kotisivu.dnainternet.net/askok/bin/lanes/.
2. Funkce umožňující práci s koprogramy
V programovacím jazyce Lua není podpora pro práci s koprogramy implementována formou speciální syntaktické konstrukce jazyka tak, jako je tomu v některých dalších programovacích jazycích implementujících koprogramy (neexistuje zde například klíčové slovo pro přerušení práce koprogramu, zatímco třeba Python pro podobnou činnost rezervuje slovo yield), ale – jak se již v Lua stalo dobrým zvykem – je vše řešeno pomocí funkcí a asociativních polí. Samotný koprogram je ve své podstatě pojmenovaná či anonymní funkce s vlastním zásobníkem, který je oddělený od zásobníku volajícího programu. Pro vytváření, řízení a zjišťování stavů koprogramů lze využít šest funkcí ležících v prostoru jmen nazvaném coroutine (prostor jmen není nic jiného než takto pojmenované globální asociativní pole se šesti „veřejnými“ funkcemi a několika pomocnými atributy), jejichž význam je uveden v následující tabulce. Některé níže vypsané funkce budou použity i v demonstračních příkladech popsaných v navazujících kapitolách:
Název funkce | Význam |
---|---|
coroutine.create() | vytvoření koprogramu |
coroutine.resume() | spuštění či znovuspuštění koprogramu |
coroutine.running() | funkce vrátí právě běžící koprogram (pro hlavní vlákno se vrací nil) |
coroutine.status() | funkce vrátí aktuální stav koprogramu – zda běží, je pozastaven či zda je běh koprogramu již ukončen |
coroutine.wrap() | vytvoření koprogramu, vrací se funkce, která koprogram spustí |
coroutine.yield() | pozastavení koprogramu a případný přenos parametrů volajícímu programu |
3. Stavy koprogramu
Koprogram se v určitém čase může nacházet v jednom ze čtyř stavů: running, normal, suspended či dead. Stav koprogramu lze kdykoli zjistit zavoláním výše uvedené funkce coroutine.status(), která vrátí stav koprogramu jako řetězec: „running“, „normal“, „suspended“ popř. „dead“. Ve stavu „running“ se může nacházet vždy jen jeden koprogram, a to ten, který funkci coroutine.status() volá. Ve stavu „suspended“ se nachází koprogram po svém vytvoření funkcí coroutine.create() či coroutine.wrap(), nebo ve chvíli, kdy je uvnitř jeho těla zavolána funkce „coroutine.yield()“, tj. koprogram je pozastaven a čeká na své znovuspuštění. Stav „normal“ je nastaven u aktivního koprogramu, který zavolal jiný koprogram (a čeká tedy na přerušení či ukončení jeho běhu), kdežto stav „dead“ je vrácen pro již neaktivní koprogram, tj. koprogram, jehož tělo bylo ukončeno příkazem return nebo se došlo na jeho konec (zde se nachází implicitní return, podobně jako u běžných funkcí).
V prvním demonstračním příkladu je ukázán způsob vytvoření koprogramu pomocí coroutine.create(), jeho spuštění funkcí coroutine.resume() a dotaz na aktuální stav koprogramu voláním funkce coroutine.status(). Vzhledem k tomu, že samotné tělo koprogramu, které je představované funkcí funkceKoprogramu(), neobsahuje volání coroutine.yield(), má tento vzorový koprogram stejné chování, jako běžný podprogram, i když jeho interní struktura je odlišná (zejména kvůli existenci samostatného běhového zásobníku):
-- Prvni demonstracni priklad prace s koprogramy:
-- vytvoreni koprogramu, vypis jeho stavu a spusteni
-- funkce, pro kterou bude koprogram vytvoren
function funkceKoprogramu()
print("*** Koprogram byl spusten ***")
end
-- vytvoreni koprogramu
koprogram = coroutine.create(funkceKoprogramu)
-- vypis typu objektu
print("Typ objektu:", koprogram)
-- zjisteni a vypis stavu koprogramu
print("Stav koprogramu:", coroutine.status(koprogram))
-- spusteni koprogramu
coroutine.resume(koprogram)
-- zjisteni a vypis stavu koprogramu
print("Stav koprogramu:", coroutine.status(koprogram))
-- finito
Výše uvedený skript po svém spuštění vypíše na standardní výstup tyto informace:
Typ objektu: thread: 0x80519c8
Stav koprogramu: suspended
*** Koprogram byl spusten ***
Stav koprogramu: dead
Většinou se však v Lua skriptech setkáme s odlišným způsobem zápisu koprogramu, ve kterém je jeho tělo zapsáno formou anonymní funkce přímo ve volání coroutine.create() či coroutine.wrap() – není totiž důvod, aby byla funkce pojmenovaná, neboť je „schována“ uvnitř koprogramu. Druhý demonstrační příklad po svém spuštění vypíše stejný text, jako příklad první:
-- Druhy demonstracni priklad prace s koprogramy:
-- vytvoreni koprogramu, vypis jeho stavu a spusteni
-- (anonymni) funkce koprogramu je vytvorena primo
-- ve volani coroutine.create()
-- vytvoreni koprogramu s vyuzitim anonymni funkce
koprogram = coroutine.create(
function()
print("*** Koprogram byl spusten ***")
end
)
-- vypis typu objektu
print("Typ objektu:", koprogram)
-- zjisteni a vypis stavu koprogramu
print("Stav koprogramu:", coroutine.status(koprogram))
-- spusteni koprogramu
coroutine.resume(koprogram)
-- zjisteni a vypis stavu koprogramu
print("Stav koprogramu:", coroutine.status(koprogram))
-- finito
4. Přerušení koprogramu a obnova jeho běhu
Ve druhé kapitole jsme si řekli, že pro (dočasné) přerušení běhu koprogramu se používá funkce coroutine.yield(). Tato funkce se může nacházet v takřka libovolném místě těla koprogramu, včetně programových smyček. Ve chvíli, kdy se tato funkce zavolá, je běh koprogramu přerušen a řízení se vrátí zpět do volající části programu (tou může být buď hlavní vlákno, nebo libovolný jiný koprogram). Spuštění či obnova běhu koprogramu se provede zavoláním funkce coroutine.resume(), jejímž parametrem je instance koprogramu (z předchozích částí tohoto seriálu již víme, že mezi datové typy jazyka Lua náleží i typ thread, což není nic jiného, než instance koprogramu získaná pomocí coroutine.create()). V následujícím demonstračním příkladu je koprogram nejprve spuštěn funkcí coroutine.resume() (na standardní výstup se vypíše první řetězec začínající třemi hvězdičkami), posléze je běh koprogramu přerušen zavoláním coroutine.yield() s tím, že řízení je předáno volajícímu programu a následně je běh koprogramu obnoven od místa svého přerušení opětovným zavoláním funkce coroutine.resume(), což se projeví výpisem druhého řetězce začínajícího třemi hvězdičkami:
-- Treti demonstracni priklad prace s koprogramy:
-- vytvoreni koprogramu, zavolani coroutine.yield()
-- a nasledne coroutine.resume()
-- vytvoreni koprogramu s vyuzitim anonymni funkce
koprogram = coroutine.create(
function()
print("*** telo koprogramu pred zavolanim yield ***")
coroutine.yield()
print("*** telo koprogramu po zavolani yield ***")
end
)
-- vypis typu objektu
print("Typ objektu:", koprogram)
-- zjisteni a vypis stavu koprogramu
print("Stav koprogramu:", coroutine.status(koprogram))
-- spusteni koprogramu
coroutine.resume(koprogram)
-- zjisteni a vypis stavu koprogramu
print("Stav koprogramu:", coroutine.status(koprogram))
-- spusteni koprogramu
coroutine.resume(koprogram)
-- zjisteni a vypis stavu koprogramu
print("Stav koprogramu:", coroutine.status(koprogram))
-- finito
Třetí demonstrační příklad po svém spuštění vypíše na standardní výstup následující řádky:
Typ objektu: thread: 0x8051270
Stav koprogramu: suspended
*** telo koprogramu pred zavolanim yield ***
Stav koprogramu: suspended
*** telo koprogramu po zavolani yield ***
Stav koprogramu: dead
Čtvrtý demonstrační příklad je již poněkud složitější, protože volání „přerušovací“ funkce coroutine.yield() je v těle koprogramu umístěno uvnitř programové smyčky, tj. při každém průchodu smyčkou dojde k jeho pozastavení a předání řízení volajícímu kódu. Koprogram je spouštěn a znovuspouštěn ve smyčce typu while, protože funkce coroutine.resume() vrací pravdivostní hodnotu true či false podle toho, zda se znovuspuštění koprogramu podařilo či zda při jeho běhu nenastala nějaká chyba (v případě, že se dojde na konec koprogramu, vrací se hodnota false):
-- Ctvrty demonstracni priklad prace s koprogramy:
-- vytvoreni koprogramu, ktery 10x zavola coroutine.yield()
-- a sam sebe tak prerusi
-- koprogram je ve smycce obnovovan pomoci coroutine.resume()
-- vytvoreni koprogramu s vyuzitim anonymni funkce
koprogram = coroutine.create(
function()
for i=1, 10 do
print("*** telo koprogramu pred zavolanim yield: "..i.." ***")
coroutine.yield()
print("*** telo koprogramu po zavolani yield: "..i.." ***")
end
end
)
-- vypis typu objektu
print("Typ objektu:", koprogram)
-- zjisteni a vypis stavu koprogramu
print("Stav koprogramu pred vstupem do smycky while:", coroutine.status(koprogram))
counter = 0
-- spusteni a znovuspusteni koprogramu
while coroutine.resume(koprogram) do
counter = counter + 1
print("Funkce coroutine.resume() volano "..counter.."x")
print("Stav koprogramu ve smycce while:", coroutine.status(koprogram))
end
-- zjisteni a vypis stavu koprogramu
print("Stav koprogramu po ukonceni smycky while:", coroutine.status(koprogram))
-- finito
Následuje ukázka zpráv, které čtvrtý demonstrační příklad po svém spuštění vypíše:
Typ objektu: thread: 0x8051ea0
Stav koprogramu pred vstupem do smycky while: suspended
*** telo koprogramu pred zavolanim yield: 1 ***
Funkce coroutine.resume() volano 1x
Stav koprogramu ve smycce while: suspended
*** telo koprogramu po zavolani yield: 1 ***
*** telo koprogramu pred zavolanim yield: 2 ***
Funkce coroutine.resume() volano 2x
Stav koprogramu ve smycce while: suspended
*** telo koprogramu po zavolani yield: 2 ***
*** telo koprogramu pred zavolanim yield: 3 ***
Funkce coroutine.resume() volano 3x
Stav koprogramu ve smycce while: suspended
*** telo koprogramu po zavolani yield: 3 ***
*** telo koprogramu pred zavolanim yield: 4 ***
Funkce coroutine.resume() volano 4x
Stav koprogramu ve smycce while: suspended
*** telo koprogramu po zavolani yield: 4 ***
*** telo koprogramu pred zavolanim yield: 5 ***
Funkce coroutine.resume() volano 5x
Stav koprogramu ve smycce while: suspended
*** telo koprogramu po zavolani yield: 5 ***
*** telo koprogramu pred zavolanim yield: 6 ***
Funkce coroutine.resume() volano 6x
Stav koprogramu ve smycce while: suspended
*** telo koprogramu po zavolani yield: 6 ***
*** telo koprogramu pred zavolanim yield: 7 ***
Funkce coroutine.resume() volano 7x
Stav koprogramu ve smycce while: suspended
*** telo koprogramu po zavolani yield: 7 ***
*** telo koprogramu pred zavolanim yield: 8 ***
Funkce coroutine.resume() volano 8x
Stav koprogramu ve smycce while: suspended
*** telo koprogramu po zavolani yield: 8 ***
*** telo koprogramu pred zavolanim yield: 9 ***
Funkce coroutine.resume() volano 9x
Stav koprogramu ve smycce while: suspended
*** telo koprogramu po zavolani yield: 9 ***
*** telo koprogramu pred zavolanim yield: 10 ***
Funkce coroutine.resume() volano 10x
Stav koprogramu ve smycce while: suspended
*** telo koprogramu po zavolani yield: 10 ***
Funkce coroutine.resume() volano 11x
Stav koprogramu ve smycce while: dead
Stav koprogramu po ukonceni smycky while: dead
5. Inicializace koprogramu pomocí parametrů
V předchozích kapitolách jsme si řekli, že koprogram je vytvářený pomocí konstruktorů coroutine.create() či coroutine.wrap(), kterým se předává pojmenovaná či anonymní funkce, jenž představuje tělo koprogramu. Jistě by bylo žádoucí této funkci předávat nějaké inicializační parametry, například proměnné (asociativní pole apod.), se kterými může koprogram pracovat, názvy souborů či databázových připojení atd. To je samozřejmě možné – při spouštění či znovuspouštění koprogramu lze pomocí coroutine.resume() předat koprogramu libovolné množství parametrů. Jejich zpracování se liší – pokud je koprogram spouštěn od svého začátku, jsou tyto parametry předávány tak, jak je to u funkcí běžné – pomocí pojmenovaných argumentů v její hlavičce, v opačném případě se parametry předají jako návratová hodnota funkce coroutine.yield (viz další kapitola). První popisované chování je ukázáno v dalším demonstračním příkladu, ve kterém první volání coroutine.resume() obsahuje nepovinný parametr, kterým se určuje počet opakování smyčky uvnitř těla koprogramu. Následuje výpis zdrojového kódu pátého demonstračního příkladu:
-- Paty demonstracni priklad prace s koprogramy:
-- predani parametru koprogramu pri jeho prvnim spusteni
-- vytvoreni koprogramu s vyuzitim anonymni funkce
koprogram = coroutine.create(
function(opakovani)
print("Predany pocet opakovani: ", opakovani)
if opakovani <= 1 then
return
end
for i=1, opakovani do
print("*** telo koprogramu pred zavolanim yield: "..i.." ***")
coroutine.yield()
print("*** telo koprogramu po zavolani yield: "..i.." ***")
end
end
)
-- vypis typu objektu
print("Typ objektu:", koprogram)
-- zjisteni a vypis stavu koprogramu
print("Stav koprogramu pred jeho spustenim:", coroutine.status(koprogram))
coroutine.resume(koprogram, 5)
print("Stav koprogramu pred vstupem do smycky:", coroutine.status(koprogram))
counter = 0
-- spusteni a znovuspusteni koprogramu
while coroutine.resume(koprogram) do
counter = counter + 1
print("Funkce coroutine.resume() volana "..counter.."x")
print("Stav koprogramu ve smycce while:", coroutine.status(koprogram))
end
-- zjisteni a vypis stavu koprogramu
print("Stav koprogramu po ukonceni smycky while:", coroutine.status(koprogram))
-- finito
Hlášení vypsané pátým demonstračním příkladem na standardní výstup po jeho spuštění:
Typ objektu: thread: 0x80520f0
Stav koprogramu pred jeho spustenim: suspended
Predany pocet opakovani: 5
*** telo koprogramu pred zavolanim yield: 1 ***
Stav koprogramu pred vstupem do smycky: suspended
*** telo koprogramu po zavolani yield: 1 ***
*** telo koprogramu pred zavolanim yield: 2 ***
Funkce coroutine.resume() volana 1x
Stav koprogramu ve smycce while: suspended
*** telo koprogramu po zavolani yield: 2 ***
*** telo koprogramu pred zavolanim yield: 3 ***
Funkce coroutine.resume() volana 2x
Stav koprogramu ve smycce while: suspended
*** telo koprogramu po zavolani yield: 3 ***
*** telo koprogramu pred zavolanim yield: 4 ***
Funkce coroutine.resume() volana 3x
Stav koprogramu ve smycce while: suspended
*** telo koprogramu po zavolani yield: 4 ***
*** telo koprogramu pred zavolanim yield: 5 ***
Funkce coroutine.resume() volana 4x
Stav koprogramu ve smycce while: suspended
*** telo koprogramu po zavolani yield: 5 ***
Funkce coroutine.resume() volana 5x
Stav koprogramu ve smycce while: dead
Stav koprogramu po ukonceni smycky while: dead
6. Předávání parametrů mezi koprogramem a volající funkcí
V dnešním posledním demonstračním příkladu je ukázán způsob obousměrné komunikace mezi volajícím programem a koprogramem. Pro přenos dat lze využít funkce coroutine.resume() i coroutine.yield(). Obě tyto funkce akceptují libovolný počet parametrů (u coroutine.resume() je navíc první parametr s instancí koprogramu povinný, zbylé parametry jsou volitelné), takže je možné předat libovolné množství údajů, nezávisle na jejich typu. Koprogram v šestém příkladu převezme dva parametry a, b a pomocí funkce coroutine.yield() vrátí dvojici numerických hodnot obsahujících součet a rozdíl těchto parametrů (tato dvojice představuje návratovou hodnotu funkce coroutine.resume(), ke které je navíc přidán pravdivostní příznak představující výsledek běhu koprogramu – viz předchozí kapitoly). Po znovuspuštění koprogramu se vypočítá součin a rozdíl obou parametrů, které jsou vráceny pomocí příkazu return na konci těla koprogramu (po návratu pomocí return se koprogram již nachází ve stavu „dead“, což lze zjistit pomocí coroutine.stavus()).
-- Sesty demonstracni priklad prace s koprogramy:
-- predavani parametru mezi koprogramem a volajici funkci
-- vytvoreni koprogramu s vyuzitim anonymni funkce
koprogram = coroutine.create(
function(a,b)
print("Predane parametry do koprogramu: ", a, b);
coroutine.yield(a + b, a - b)
print("*** telo koprogramu po zavolani yield")
return a * b, a / b
end
)
-- vypis typu objektu
print("Typ objektu:", koprogram)
-- zjisteni a vypis stavu koprogramu
print("Stav koprogramu pred jeho spustenim:", coroutine.status(koprogram))
print("Vysledek prvniho volani resume: ", coroutine.resume(koprogram, 1, 2))
print("Vysledek druheho volani resume: ", coroutine.resume(koprogram))
-- zjisteni a vypis stavu koprogramu
print("Stav koprogramu po jeho ukonceni:", coroutine.status(koprogram))
-- finito
Výsledek běhu šestého demonstračního příkladu:
Typ objektu: thread: 0x8051250
Stav koprogramu pred jeho spustenim: suspended
Predane parametry do koprogramu: 1 2
Vysledek prvniho volani resume: true 3 -1
*** telo koprogramu po zavolani yield
Vysledek druheho volani resume: true 2 0.5
Stav koprogramu po jeho ukonceni: dead
7. Zdrojové kódy všech demonstračních příkladů
V následující tabulce jsou uloženy odkazy na zdrojové kódy všech šesti demonstračních příkladů popsaných v předchozích kapitolách. Taktéž jsou zde uvedeny odkazy na výsledky běhu testovacích příkladů – jedná se o standardní výstup přesměrovaný běžnými prostředky operačního systému do souboru po převodu tabulátorů na mezery:
8. Odkazy na Internetu
- Lambda the Ultimate: Coroutines in Lua,
http://lambda-the-ultimate.org/node/438 - Coroutines Tutorial,
http://lua-users.org/wiki/CoroutinesTutorial - Lua Coroutines Versus Python Generators,
http://lua-users.org/wiki/LuaCoroutinesVersusPythonGenerators - Programming in Lua 9.1 – Coroutine Basics,
http://www.lua.org/pil/9.1.html - Wikipedia CZ: Koprogram,
http://cs.wikipedia.org/wiki/Koprogram - Wikipedia EN: Coroutine,
http://en.wikipedia.org/wiki/Coroutine - Lua Lanes,
http://kotisivu.dnainternet.net/askok/bin/lanes/
9. Obsah dalšího pokračování seriálu
V následující části seriálu o programovacím jazyku Lua si popíšeme funkce a proměnné dostupné ve standardních knihovnách dodávaných společně s překladačem a interpretrem tohoto jazyka. Taktéž se budeme zabývat několika užitečnými externími knihovnami, frameworky a rozšířeními samotného jazyka (typickou ukázkou sémantického rozšíření jazyka je Metalua). Externích knihoven a jazykových rozšíření v současnosti existuje již značné množství, od knihoven určených pro numerické výpočty (Numeric Lua, která je mimochodem založena na části skvělé Netlib), přes různé grafické a multimediální knihovny, knihovny použitelné pro tvorbu grafického uživatelského rozhraní (GTK+ apod.) a knihovny pro tvorbu her až po webové frameworky, například poměrně úspěšný projekt Kepler.