Hlavní navigace

Squeak: návrat do budoucnosti (14)

11. 5. 2004
Doba čtení: 7 minut

Sdílet

Za celou řadu svých unikátních schopností vděčí Smalltalk tomu, že sám v sobě řeší správu procesů. V tomto dílu si přiblížíme důsledky tohoto kroku, představíme si principy squeakovského plánovače a naučíme se vytvářet a synchronizovat vlákna.

Ve svých počátcích tvořil Smalltalk takřka celý operační systém. Implementovat do něj správu procesů bylo tedy přirozeným krokem. V dnešní době, kdy máme k dispozici celou řadu operačních systémů s vypiplanými plánovači procesů, tato vlastnost Smalltalku zůstala. Proč?

Důvodů je celá řada a v konečném důsledku se i přes jistou ztrátu výkonu tento krok bohatě vyplácí. V první řadě se tak výrazně usnadňuje přenositelnost Smalltalku na jiné platformy. Autoři virtuálních strojů pro různé platformy se nemusí složitě snažit přizpůsobovat správě procesů hostitelskému systém nebo si ji dokonce vytvářet vlastní, pokud systém žádnou nemá. Virtuální stroje na různých platformách si tak zachovávají přibližně stejnou složitost, a co se procesů týče, anulují se veškeré problémy s přenositelností image. Pro plnohodnotný běh Squeaku bohatě stačí jediné vlákno.

Dalším dobrým důvodem je flexibilita tohoto přístupu. Při troše znalostí není větší problém přizpůsobit si správu procesů potřebám konkrétní aplikace a dané situace. Málokde dostanete takovou příležitost za běhu systému tak snadno modifikovat tuto jeho niternou součást. I když vám v tom nikdo bránit nebude, nelze samozřejmě předpokládat, že budete prohazovat plánovač procesů za jeho jinou vlastní verzi. Nicméně pokud se některou implementaci Smalltalku rozhodnete použít např. jako webový či databázový server v enterprise nasazení, jistě jeho přizpůsobivost uvítáte.

Pro programátora zřejmě největší přínos představují ladící možnosti, správa výjimek, zachování integrity systému a dech beroucí jednoduchost tvorby vícevláknových aplikací.

Vytvoření procesu

Squeak nerozlišuje mezi vlákny a procesy. Pracuje pouze s procesy (třída Process), ty však svou podstatou odpovídají spíše vláknům. Nejjednodušší cestou, jak vytvořit nový proces, je zaslat bloku zprávu fork

[ 1000 factorial ] fork.

K vyhodnocení bloku dojde v novém procesu běžícím se stejnou prioritou, jakou měl rodičovský proces. Výsledkem tohoto výrazu je nově vytvořený proces, který je okamžitě označen jako runnable, tedy ihned připraven pro naplánování.

Pro spuštění procesu pod specifikovanou prioritou slouží zpráva bloku forkAt:. Pokud má rodičovský proces počkat na ukončení nového procesu, pomůže nám k tomu zpráva forkAndWait.

[ 1 to: 100 do: [:i | Transcript show: i; cr ] ]
forkAt: Processor userBackgroundPriority.

[ 1234 factorial ] forkAndWait

Pro vytvoření nového procesu se používá metoda newProcess třídy BlockContext. Vygenerovaný proces je na rozdíl od forkovaného procesu neaktivní (suspended). Můžeme mu proto nastavit potřebné vlastnosti a spustit jej pomocí zprávy resume.

| process |
process := [
     1 to: 100 do: [:i | Transcript show: i; cr ]
] newProcess.
process priority: Processor highIOPriority.
process resume.

Globální proměnná Procesor je jedinečná instance třídy ProcessorScheduler, tedy plánovače procesů.

Priorita

Priorita procesu je celé číslo od jedné po hodnotu Procesor highestPriority. Čím vyšší hodnotu má, tím má proces větší prioritu. Existuje několik standardně pojmenovaných priorit, které jsou přístupné jako zprávy třídy ProcesorScheduller.

Tabulka č. 561
číslo jméno účel
80 timingPriority real-time procesy
70 highIOPriority procesy pro zpracovávání kritických vstupů a výstupů (např. ze sítě)
60 lowIOPriority procesy pro většinu vstupů a výstupů (např. z klávesnice)
50 userInterruptPri­ority vysokoprioritní uživatelské procesy
40 userSchedulin­gPriority běžné uživatelské procesy, priorita plánovače GUI
30 userBackgroundPri­ority nízká uživatelská priorita
20 systemBackgrou­ndPriority systémové procesy běžící na pozadí
10 lowestPriority nejnižší priorita, klidové procesy

Aktuální prioritu lze získat pomocí příkazu Processor activePriority.

Stavy procesů

Procesy se mohou nacházet v pěti stavech – suspended, runnable, running, terminated awaiting.

Ve stavu suspended je proces neaktivní a nežádá o přidělení procesoru. Jak jsme si řekli, v tomto stavu se nachází po vytvoření zprávou newProcess. Je schopen dostat se z něj jen v případě, že mu jiný proces pošle zprávu resume. K suspendování procesu se používá zpráva suspend.

Ve stavu runnable je proces připraven k běhu a čeká na přidělení procesoru. Tento proces může být zprávou terminate od jiného procesu násilně ukončen.

Ve stavu running je procesu přiřazen procesor a stává se z něj aktivní proces. Sám se pak může pomocí zprávy yield procesoru vzdát ve prospěch jiných procesů. Zprávou terminate se může ukončit a zprávou suspend uspat.

Aktivní proces lze získat zprávou Processor activeProcess. Protože je vždy aktivní pouze jeden proces, tedy ten, který je jako jediný schopen tento příkaz vykonat, získá tak běžící proces sám sebe.

Kromě těchto čtyř základních stavů se může proces nacházet ještě ve stavu waiting, kdy čeká na semafor (viz dále).

Plánovač

Princip squeakovského plánovače procesů je poměrně jednoduchý. Procesoru je vždy přiřazen proces s nejvyšší prioritou schopný běhu. V okamžiku, kdy běží nějaký proces a je vytvořen či oživen proces s vyšší prioritou, je mu procesor odepřen ve prospěch důležitějšího kolegy.

Plánovač procesů tvoří pole, kde každé jeho položce odpovídá číslo priority. Každé této prioritě je pak přiřazen cyklický seznam naplánovaných procesů. V okamžiku, kdy se nějaký proces dobrovolně vzdá procesoru či je k tomuto kroku přinucen nově aktivovaným procesem s vyšší prioritou, zařadí se na konec fronty.

Pro procesy s různými prioritami se tak používá preemptivní plánování, pro procesy se stejnou prioritou kooperativní. Stačí, aby se v systému vyskytoval jeden vysokoprioritní proces, který nedělá nic jiného, než že se probudí a na určité časové kvantum se opět uspí, čímž se zajistí tak poměrně spravedlivé přiřazování času procesoru. Při uspání totiž není procesor přidělen opět stejnému procesu, protože ten byl přinucen se ho vzdát a zařadit se do fronty čekajících procesů.

Semafor

Semafor je základním objektem sloužícím k synchronizaci procesů. Pošle-li aktivní proces semaforu zprávu wait, je přepnut do stavuwaiting, ve kterém vyčkává, dokud jiný proces semaforu nezašle zprávu signal.

| semaphore var1 var2 |
semaphore := Semaphore new.
[
     var2 := 100 factorial.
     semaphore signal.
] fork.
var1 := 10 factorial.
semaphore wait.
var1 := var1 + var2.

V tomto příkladu necháváme faktoriál většího čísla vypočítat v separátním procesu. Semafor je použit pro čekání na korektní výsledek v proměnné var2.

Pokud na semaforu vyčkává více procesů, je signálem uvolněn vždy jen jeden – ten, který na semaforu čekal nejdéle. Priority procesů při tom nehrají žádnou roli.

Pro zájemce připomínám, že kromě semaforu je ve Squeaku rovněž implementován monitor.

Kritická sekce

Semafory se často používají pro vzájemné vyloučení pomocí kritické sekce. V kritické sekci se v jeden okamžik smí vyskytovat pouze jeden proces. Její význam si ukážeme na následujícím příkladu:

| var  |
var := 0.
[
     1 to: 10000 do: [:i |
          var := var +1.
          Processor yield
     ].
] fork.
[ var < 5000  ]
     whileTrue: [ Processor yield ].

Máme proměnnou var, ke které zároveň přistupují dva procesy. V jednom vlákně čekáme, dokud nebude její hodnota větší než pět tisíc. V druhém vlákně tuto hodnotu zvýšíme o deset tisíc. Deset tisíc je velké číslo. Abychom se nepředřeli, nezvýšíme ji najednou, ale postupným přičítáním jedničky. Průběžně se příkazem Processor yield vzdáváme procesoru, aby byly spravedlivě obslouženy oba procesy.

V tomto případě samozřejmě obdržíme v proměnné var jako výsledek hodnotu 5000, což ovšem není v souladu s naším záměrem ji atomicky zvednout o 10000.

Použijeme tedy semafor pro vzájemné vyloučení. Ten se vytvoří výrazem Semaphore forMutualExclusion, kterému je na rozdíl od běžného semaforu na začátku jeho existence zaslána zpráva signal. Kritickou sekci poté definujeme pomocí zprávy critical:

| cs var  |
cs := Semaphore forMutualExclusion.
var := 0.
[
     cs critical: [
          1 to: 10000 do:
               [:i | var := var +1 ].
     ]
] fork.
[ cs critical: [var < 5000 ] ]
     whileTrue: [ Processor yield ].

Nyní se inkrementace již provede atomicky, proto jako výsledek získáme číslo 10000.

Delay

Třída Delay slouží k přerušování procesu na určitou dobu. Vytváří se např. pomocí konstruktorů forSeconds: nebo forMilliseconds:. Po vytvoření její instance ovšem proces nečeká. K tomu dojde až v okamžiku, kdy jí zašleme zpávu wait.

(Delay forSeconds: 5) wait.

Sdílené fronty

Sdílené fronty (třída SharedQueue) jsou speciální kolekce, které jsou při zápisu a čtení chráněny semafory.

| sq |
sq := SharedQueue new.
[
     1 to: 10 do: [:i |
          sq nextPut: i.
     ]
] fork.
[
     11 to: 20 do: [:i |
          sq nextPut: i.
     ]
] fork.
[
     20 timesRepeat: [
          Transcript show: sq next; cr.
     ]
] fork.

V tomto příkladu máme tři procesy, které pracují s jednou sdílenou frontou. Dva do ní zapisují data pomocí zprávy nextPut: a jeden z ní tato data pomocí zprávy next čte a vypisuje do Transcriptu. V případě, že je vyžadováno čtení z fronty (next), ale fronta je prázdná, je čtecí proces uspán do chvíle, než je do fronty opět nějaký prvek vložen. SharedQueue mimo next a nextPut:rozumí i dalším zprávám, jako je peek, nextOrNil, flush apod. Na to, zda je fronta prázdná, se můžeme zeptat zprávou isEmpty.

Poznámky

Pro lepší odezvu systému se u vysokoprioritních procesů doporučuje, aby se, pokud je to možné, často vzdávaly procesoru pomocí příkazuProcessor yield ve prospěch svých méně šťastných kolegů.

Pokud vyžadujeme, aby byl nějaký veledůležitý výpočet proveden bez přerušování, můžeme k tomu využít zprávu valueUnpreemptively zaslanou bloku.

Proces s nejnižší prioritou se ve Squeaku chová tak, že se pokouší pomocí primitivy uvolnit procesor hostitelského systému, aby Squeak zbytečně nezabíral strojový čas.

CS24_early

K inspekci procesů slouží nástroj Process Browser. Procesy jsou v něm organizovány ve skupinách podle priority. Po zapnutí tzv.CPUWatcher lze sledovat, kolik procent času procesoru které procesy zabírají. Pokud chcete stav sledovat průběžně, zapněte si volbu auto-update.

Bez použití kritických sekcí je atomicita zaručena pouze pro vykonávání jedné instrukce bytekódu.

Byl pro vás článek přínosný?