background image

klastry obliczeniowe

34

maj 2004

Rozproszone fraktale

Marek Sawerwain

O

soby,  które  zajmują  się 
grafiką,  a  w  szczególno-
ści  techniką  śledzenia 
promieni,  często  mogą 

powiedzieć,  że  moc  obliczeniowa  ich 
komputerów  nie  jest  wystarczająca. 
Dotyczy  to  także  rysowania  fraktali, 
szczególnie  tych  o  skomplikowanych 
wzorach.  W  tym  artykule  chciałbym 
pokazać,  jak  rozdzielić  zadanie  ryso-
wania  tego  typu  obiektów  na  kilka 
komputerów  za  pomocą  technologii 
rozproszonych  obiektów  –  CORBA. 
Napiszemy  nieskomplikowany  program 
tworzący coś w rodzaju klastra przezna-
czonego do rysowania fraktali.

Wbrew  pozorom,  proces  rysowa-

nia  fraktali  jest  bardzo  pracochłonny 
i  tylko  najbardziej  popularne  zbiory, 
takie jak MandelbrotJulia czy Newton
można  narysować  w  czasie  rzeczywi-
stym.  Po  wprowadzeniu  kilku  uprosz-
czeń,  można  nawet  pokusić  się  o  ich 
animację.  W  innych  przypadkach  jest 
to  proces  bardzo  czasochłonny.  Dość 
łatwo  można  go  przyspieszyć,  gdy 
sam  proces  rysowania  podzielimy  na 
regiony  i  rozdzielimy  na  kilka  kompu-
terów.

Kilka postanowień na 

początek

Pierwsza  decyzja,  którą  trzeba  podjąć, 
dotyczy  języków  programowania  uży-
tych do napisania systemu. Serwer zosta-
nie napisany przy pomocy języka Python 
i systemu CORBA OmniORB.

Dlaczego  Python?  Nasz  system, 

pomimo  swoich  niewielkich  rozmiarów, 
ma  być  systemem  uniwersalnym  i  zdol-

nym  do  rysowania  dowolnych  fraktali 
opartych o tzw. funkcję ucieczki. Python, 
jak  każdy  język  skryptowy,  nie  oferuje 
wysokiej wydajności, ale posiada pewne 
interesujące  nas  cechy,  a  jedną  z  nich 
jest  możliwość  zdefiniowania  funkcji 
w  czasie  wykonania  skryptu.  Dzięki  tej 
własności  serwer  będzie  odbierał  od 
klienta kod źródłowy funkcji i instalował 
ją we własnym środowisku.

Powód,  dla  którego  za  system 

CORBY  po  stronie  serwera  został 
wybrany  pakiet  OmniORB,  jest  bardzo 
prozaiczny.  Obecnie  oferuje  on  najlep-
szą  wersję  CORBY  dla  języka  Python. 
Można  w  nim  bez  zbędnych  kłopotów 
korzystać z usługi nazw, która ma zna-
czenie  zasadnicze  dla  naszego  klastra 
rysującego fraktale.

Zastanówmy  się  teraz  nad  progra-

mem  klienta.  Może  on  być  napisany 
w dowolnym języku i dowolnym systemie 
CORBY, zgodnym ze standardem adapte-
ra  POA  oraz  obsługującym  dynamiczne 
wywołania  metod  (DII  –  Dynamic 
Invocation  Inteface
).  Takim  serwerem 
jest również OmniORB, ale aby pokazać 
elastyczność  technologii  rozproszonych 
obiektów,  zastosujemy  inny  system 
o  nazwie  MICO.  Oprócz  zastosowania 
innego systemu CORBA, program klienta 
zostanie napisany w innym języku, gdyż 
będzie to C++.

Ostatni  problem  to:  w  jaki  sposób 

rozproszyć proces rysowania fraktala na 
kilka  komputerów?  Zastosowany  algo-
rytm rysuje fraktal linia po linii. Oznacza 
to, że możemy bez problemów podzielić 
cały  proces  rysowania  na  kilkanaście 
komputerów rozdzielając im poszczegól-

O autorze

Autor zajmuje się tworzeniem 

oprogramowania dla WIN32 

i Linuksa. Zainteresowania: 

teoria języków programowania 

oraz dobra literatura. Kontakt 

z autorem: 

autorzy@linux.com.pl.

Na płycie CD/DVD

Na płycie CD/DVD znajdują 

się pliki źródłowe napisanego 

programu, jak również wszystkie 

listingi.

background image

35

www.linux.com.pl

klastry obliczeniowe

corba

ne  wiersze  rysowanego  obrazu.  Klient 
przy pomocy usługi nazw pobierze listę 
zarejestrowanych  węzłów  i  do  każdego 
wyśle  odpowiednią  ilość  wierszy  do 
narysowania.  Po  otrzymaniu  wyników 
połączy  pojedyncze  wiersze  w  jeden 
rysunek. Schematycznie zostało to przed-
stawione na Rysunku 1.

Definicja interfejsu 

dfractal

Podczas  pisania  jakichkolwiek  aplikacji 
w technologii CORBA należy w pierwszej 
kolejności określić interfejs obiektów roz-
proszonych. Nasz interfejs o nazwie 

calc_

node

 to tylko cztery metody. Jego defini-

cja znajduje się na Listingu 1. Czynności, 
które wykonują poszczególne metody, są 
następujące:

•  

setup_iter_func

  –  określa  postać 

funkcji ucieczki;

•  

setup_render_size

 – określa wymia-

ry obrazu, na którym będzie rysowa-
ny fraktal (w pikselach);

•  

render_one_row

 – rysuje wiersz o nu-

merze 

y

 od pozycji 

from_x

 do 

to_x

;

•  

quit

 – wywołanie tej funkcji powodu-

je zakończenie pracy węzła.

Posługiwanie  się  tym  interfejsem  jest 
trywialne,  np.  w  Pythonie,  gdy  chcemy 
narysować  cały  fraktal,  to  w  pierwszej 
kolejności ustalamy wymiary oraz poda-
jemy kod funkcji ucieczki:

calc_node.setup_render_size(300,200) 
calc_node.setup_iter_func(funkcja_ucieczki)

Następnie  przy  pomocy  zwykłej  pętli 

while

  wywołujemy  metodę 

render_one_

row

.  Wynikiem  tej  metody  jest  jeden

wiersz  obrazu.  Kolory  zostały  zakodo-
wane  w  postaci  trójek  RGB,  więc  mamy
gotowe dane do bezpośredniego przenie-
sienia na ekran lub zapisu do pliku:

row=0
while row<=200:
    dane=calc_node.render_one_

S

row(row,0,300)

    for i in dane:
        print i
    row=row+1

Tworzymy serwer

Aplikacje w CORBIE są dzielone na dwie 
części: serwer i klient. Prace nad naszym 
systemem zaczniemy od napisania serwe-
ra, gdyż, wbrew pozorom, to mniej skom-
plikowane zadanie.

W  pierwszej  kolejności  trzeba 

poddać  kompilacji  plik  z  interfejsem, 
gdyż  tego  wymaga  odwzorowanie 
OmniORB  dla  Pythona.  Polecenie  jest 
następujące:

omniidl -bpython dfractal.idl

Po kompilacji pliku z interfejsem, utwo-
rzone  pliki  dołączamy  słowem 

import

Dołączenie  wszystkich  modułów  konie-
cznych  do  poprawnej  pracy  serwera  to 
tylko trzy linie kodu:

from omniORB import CORBA, PortableServer
import CosNaming 
import dfractal, dfractal__POA

W  pierwszej  linijce  dołączamy  moduły 
bezpośrednio  związane  z  danym  syste-
mem  CORBY,  czyli  w  naszym  przypad-
ku OmniORB. Druga linia dołącza inter-

Rysunek 1. 

Schemat „klastra” rysującego fraktal

Instalacja pakietów 

OmniORB oraz MICO

Autorzy obydwu pakietów przygotowali 
skrypty configure, więc kompilacja prze-
biega według typowej ścieżki postępo-
wania:

./configure –prefix=/katalog/

S

gdzie/ma/zostać/zainstalowany/pakiet
make
make install

Również pakiet z wersją systemu 
OmniORB dla języka Python o nazwie 
omniORBpy posiada skrypt configure. 
Oczywiście, warto mieć zainstalowane-
go Pythona w stosunkowo nowej wersji 
2.2 albo 2.3.

background image

36

maj 2004

klastry obliczeniowe

fejs  odpowiedzialny  za  usługę  nazw. 
W  trzeciej  linii  dołączamy  nasz  interfejs 
oraz  wygenerowany  szkielet  do  imple-
mentacji serwera.

Kod  całego  serwera  znajduje  się  na 

płycie  CD/DVD,  ale  Listing  2  zawiera 
jego najważniejsze fragmenty. Jak widać, 
została usunięta definicja klasy 

Fractal-

Render

. Powrócimy do niej w następnym 

punkcie.

Każdy, kto choć raz napisał najmniej-

szy  program  w  technologii  CORBA,  od 
razu  pozna  charakterystyczne  elementy 
występujące w programie serwera – pro-
gramy  w  CORBIE  są  do  siebie  bardzo 
podobne,  pomimo  różnych  serwerów 
CORBY  czy  zastosowanego  języka  pro-
gramowania.

Centralnym  elementem  naszego  sys-

temu renderingu jest usługa nazw. W ser-
werze  oraz  w  kliencie  musimy  podać 
adres  symboliczny  bądź  numer  IP,  pod 
którym  znajduje  się  ta  usługa.  Z  tego 
powodu  przed  uzyskaniem  odniesienia 
do  obiektu 

orb

,  zmiennej  zawierającej 

argumenty podane w linii poleceń, wpi-
sujemy adres maszyny, na której urucho-
miono usługę nazw:

sys.argv.extend([‘-ORBInitRef’, 

S

     ‘NameService=corbaname::127.0.0.1’])

Jak  widać  z  powyższego  przykładu, 
odwołujemy  się  do  lokalnej  maszyny, 
więc używając tego systemu w rzeczywi-
stej sieci, trzeba zmienić tę linię kodu.

Wcześniej 

wykonujemy 

jeszcze 

jedną  istotną  czynność,  a  mianowicie 
odczytujemy  podaną  nazwę  nasze-
go  węzła.  Nazwy  dla  poszczególnych 
węzłów  muszą  być  oczywiście  inne  – 
w  ten  sposób  je  rozróżniamy.  Proces 
rejestracji naszego węzła w usłudze nazw 
rozpoczynamy  od  uzyskania  odniesie-
nia do usługi nazw. Pomimo, że serwer 

jest  pisany  w  Pythonie,  to  następujący 
kod jest prawie identyczny z tym, który 
omówię  przy  okazji  klienta  (napisanego 
przy pomocy języka C++):

ns = orb.resolve_initial_references 

S

   („NameService”)
root_ctx = ns._narrow 

S

   (CosNaming.NamingContext)

Sam proces rejestracji obiektu poprzedza-
my jego wstępnym utworzeniem:

servant = CalcNodeServant()
objref = servant._this()

Gdy w zmiennej 

objref

 mamy już refe-

rencję  do  obiektu,  to  wystarczy  utwo-
rzyć  obiekt  nazwy  bazujący  na  typie 

CosNaming.NameComponent

  oraz  dokonać    

jego  rejestracji  metodą 

bind

  obiektu

root_ctx

:

name = [CosNaming.NameComponent 

S

   („calc_node”, node_name)]
root_ctx.bind(name, objref)

Ostateczna aktywacja serwera to typowy 
kod  dla  dowolnego  serwera  CORBY. 
Dokonujemy  aktywacji  adaptera  POA: 

poa._get_the_POAManager().activate()

 

oraz uruchamiamy cały serwer: 

orb.run()

Jak widać na Listingu 2, pomiędzy tymi 
poleceniami  został  jeszcze  utworzony 
obiekt typu 

FractalRender

.

Dzięki zdefiniowaniu oddzielnej klasy 

do rysowania samego fraktala, poszczegól-
ne metody obiektu   

CalcNodeServant

 są 

bardzo krótkie i nie zawierają zbyt skom-
plikowanej treści. Wszystkie odwołują się 
do  globalnego  obiektu 

fractal

.  I  to  on 

wykonuje całą „robotę” związaną z obsłu-
gą procesu rysowania. Wyróżnia się tylko 
metoda 

quit

, kończąca działanie naszego 

serwera. Dokonuje ona także zwolnienia 

nazwy,  pod  którą  konkretny  serwer  jest 
reprezentowany w usłudze nazw.

Rysowanie fraktala

Rysowanie  fraktala  to  zadanie  klasy 

FractalRender

. Znaczenie poszczególnych 

metod jest następujące:

•  

ResetParam(_maxx,  _maxy)

  –  ustala 

wymiary  rysunku  fraktala  w  pik-
selach,  współrzędne  początku  oraz 
wymiary  przestrzeni  zespolonej,  na 
której powstaje fraktal;

•  

S e t u pIt e r F u n c ( c o d e _ o f_ f u n c)

 

– tworzy funkcję ucieczki (iteracji);

•  

RenderOneLine(render_y, 

from_x, 

to_x)

  –  rysuje  pojedynczy  wiersz, 

wartością wynikową jest lista z kolo-
rami w formacie RGB.

W  rzeczywistości  w  kodzie  są  jeszcze 
dwie  dodatkowe  metody: 

RenderFrag-

ment

,  przeznaczona  do  rysowania  frag-

mentu  fraktala,  oraz 

DrawFullFractal-

MandelbrotStyle

,  rysującą  cały  fraktal. 

Obydwie,  identycznie  jak 

RenderOneLi-

ne

, dają w wyniku listę z kolorami w for-

macie RGB.

Zastosowany  algorytm  należy  do 

najprostszych  technik  rysowania  frak-
tali,  która  polega  na  tym,  że  rysowanie 

Listing 1. 

Definicja interfejsu dfractal

module dfractal{

   

typedef sequence

<long> TRow;

   

interface calc_node{

      

void

 setup_iter_func(

in string code

);

     

 void

 setup_render_size(

in 

long width, 

in 

long height

);

      TRow render_one_row(

in 

long y, 

in 

long from_x, 

in 

long to_x

);

     

 void

 quit();

   };
};

Format pliku PNM

Pliki  w  formacie  PNM  (obsługiwany 
bez  problemów  przez  GIMP-a)  mają 
bardzo  prostą  strukturę.  Do  naszych 
potrzeb wystarczy plik PNM o typie P3. 
Jest to w pełni tekstowy format. Jego 
nagłówek  zaczyna  się  w  następują-
cy sposób:

P3
# CREATOR: narysowane przez dfrac-
tal
100 100

W pierwszej linii zawarty jest typ pliku. 
P3 oznacza, że będzie to w plik w for-
macie wyłącznie tekstowym. Następ-
nie podajemy komentarz – można w 
nim umieścić krótką informację, kto 
utworzył plik. W trzeciej linii nagłówka 
podajemy dwie liczby – są to wymiary 
rysunku: szerokość oraz wysokość. Po 
tak skonstruowanym nagłówku poda-
jemy kolejne wartości pikseli, opisa-
ne za pomocą trójek RGB. Poszcze-
gólne wartości znajdują się w oddziel-
nych liniach.

background image

37

www.linux.com.pl

klastry obliczeniowe

corba

w  przestrzeni  zespolonej  rozpoczynamy 
od  lewego  górnego  rogu  i  linia  po  linii 
dla  każdego  punktu  na  wydzielonym 
wcześniej  obszarze  sprawdzamy,  jaką 
wartość ma funkcja ucieczki.

Cały  kod  funkcji  rysującej  wskaza-

ny wiersz zawiera Listing 3. Ma ona trzy 
części.

W pierwszej części ustalamy wymia-

ry rysunku w pikselach: zmienne 

vWidth

 

vHeight

.  Zmienne 

vfromX

  i 

vfromY

 

zawierają  współrzędne  lewego  górne-
go  rogu.  Początkowe  wartości  zawie-
rają  zmienne  z  przedrostkiem  „new”. 
Ich  wartość  zostaje  określona  metodą 

ResetParam

  w  momencie  wywoła-

nia  metody 

setup_render_size

  obiektu 

calc_node

. Ostatnie dwa przypisania do 

kx

 i 

ky

 określają krok, o jaki będą zwięk-

szane  współrzędne  zespolone.  W  przy-
padku  rysowania  wiersza  istotna  jest 
tylko zmienna 

kx

.

W części drugiej obliczamy  przesu-

nięcia  względem  osi 

x

  oraz 

y

,  oczywi-

ście  w  przestrzeni  zespolonej  (mimo, 
że mówimy o liczbach zespolonych, to 
wszystkie  obliczenia  bazują  na  warto-
ściach  rzeczywistych).  Obliczenia  dla 
zmiennej 

yy

 polegają na tym, że podany 

numer  wiersz 

render_y

  jest  mnożo-

ny przez wartość kroku 

ky

 i dodawany 

do  zmiennej  zawierającej  współrzędne 

lewego  górnego  rogu 

vfromY

.  Ta  część 

funkcji wyznacza też początek rysowa-
nia w danym wierszu (początek zawie-
ra  zmienna 

xx

,  a  miejsce  zakończenia 

vtoX

). Oznacza to, iż metoda 

RenderOne-

Line

 umożliwia także rysowanie dowol-

nego fragmentu podanej linii.

Po 

wyznaczeniu 

współrzędnych 

danego wiersza w części trzeciej pozostaje 
nam tylko przy pomocy nieskomplikowa-
nej pętli przejść wzdłuż osi 

x

 po poszcze-

gólnych punktach należących do danego 
wiersza  i  dla  każdego  wywołać  funkcję 

iter,

  czyli  funkcję  ucieczki.  Dopiero 

w argumencie tej funkcji pojawia się kon-
struktor 

complex

, który ze współrzędnych 

xx

 i 

yy

 tworzy liczbę zespoloną.

Wynikiem  funkcji 

RenderOneLine

 

jest  lista  zawierająca  poszczególne  trójki 
RGB, opisujące kolory. Lista tworzona jest 
w bardzo prosty sposób. Na początek two-
rzymy listę pustą, a następnie po wyzna-
czeniu  wartości  kolorów  dodajemy  te 
wartości do listy. W tym miejscu podczas 
wyznaczania wartości kolorów ujawnia się 

Rysunek 2. 

Diagram przepływu 

sterowania w programie klienta

Listing 2. 

Fragmenty skryptu Pythona implementującego serwer calc_node

#! /usr/bin/python

import sys

from omniORB 

import CORBA, PortableServer

import CosNaming 

import dfractal, dfractal__POA

class

 FractalRender:

   # usunięta definicja klasy rysującej fraktal

class

 CalcNodeServant(dfractal__POA.calc_node):

      

global

 fractal

     

 print

 

„setup_iter_func”, code

      fractal.SetupIterFunc(code)
   

def

 setup_render_size(self, width, height):

      

global

 fractal

      fractal.ResetParam(width, height)
      

print

 

„setup_render_size w”,width,”h”,height

   

def

 render_one_row(self, y, from_x, to_x):

      

print

 

„begin render”,y,from_x, to_x

      r=fractal.RenderOneLine(y,from_x, to_x)
     

 print

 

„end render”,y,from_x, to_x

      

return

 r

 

  def

 quit(self):

      

global

 orb, root_ctx

      root_ctx.unbind(name)
     

 print

 

„node stop ...”

      orb.shutdown(

0

)

if

 len(sys.argv)>

1:

   node_name=sys.argv[

1

]

else:

   

print

 

„proszę podać nazwę węzła”

   sys.exit(

0)

sys.argv.extend([‘-ORBInitRef’, ‘NameService=corbaname::

127.0.0.1’]

)

orb = CORBA.ORB_init(sys.argv, CORBA.ORB_ID)
poa = orb.resolve_initial_references(

„RootPOA”

)

ns = orb.resolve_initial_references(

„NameService”

)

root_ctx = ns._narrow(CosNaming.NamingContext) 
servant = CalcNodeServant()
objref = servant._this()
name = [CosNaming.NameComponent(

„calc_node”, node_name

)]

 

try:

   root_ctx.bind(name, objref);

except

 CosNaming.NamingContext.AlreadyBound:

   root_ctx.rebind(name, objref)
   

print

 

„Name calc_node [„,node_name,”] already existed -- rebound” 

poa._get_the_POAManager().activate()
fractal=FractalRender()
orb.run()
print 

„... end work ...”

background image

38

maj 2004

klastry obliczeniowe

pewna  niedoskonałość.  Sposób  wyzna-
czania  kolorów,  który  bazuje  na  reszcie 
z  dzielenia,  nie  daje  zbyt  efektownych 
układów kolorystycznych, więc zadaniem 
dla  Czytelnika  jest  napisanie  dodatkowej 
funkcji kolorującej fraktal.

Definiowanie funkcji 

ucieczki (iteracji)

Zajmijmy się teraz metodą, która pozwa-
la  na  ustalenie  postaci  funkcji  ucieczki, 
gdyż  to  ta  funkcja  odgrywa  najważniej-
szą rolę podczas rysowania różnych frak-
tali. Treść metody 

SetupIterFunc

 wbrew 

pozorom jest zaskakująco krótka:

def SetupIterFunc(self, code_of_func): 
   code_of_iter_func=compile(code_of_

S

   func, „<string>”, „exec”)
   eval(code_of_iter_func,globals())

W  pierwszej  linii  dokonujemy  kompi-
lacji  funkcji,  więc  warto  przed  rozpo-
częciem  procesu  rysowania  dokładnie 
sprawdzić,  czy  funkcja  iteracji  została 
napisana  bezbłędnie.  Funkcję  kompilu-
jemy w tzw. trybie wykonawczym (trzeci 
parametr  przyjmuje  wartość  exec).  Po 
wykonania funkcji 

compile

 otrzymujemy 

obiekt  reprezentujący  kod,  który  można 
wykonać  przy  pomocy  funkcji 

eval

Istotne  jest,  aby  w  drugim  argumencie 
tej funkcji podać jako argument funkcję 

globals()

. Spowoduje to, iż nasza funkcja 

(o  nazwie 

iter

,  bowiem  taka  nazwa 

występuje  w  metodzie 

RenderOneLine

zostanie  zdefiniowana  w  podstawowej 
(głównej)  przestrzeni,  a  więc  będzie 
widziana w każdej klasie.

Funkcja 

iter

,  oprócz  nazwy,  przyj-

muje  tylko  jeden  parametr,  który  jest 
liczbą  zespoloną  zbudowaną  w  oparciu 
o współrzędne punktu w układzie zespo-
lonym.  Przykład  takiej  funkcji  rysującej 
zbiór Mandelbrota zawiera Listing 4.

Program klienta

Program klienta jest niewiele większy niż 
kod źródłowy serwera. Mimo, że to tylko 
5 kB tekstu, to również omówimy tylko 
jego najciekawsze fragmenty. W analizie 
programu  klienta  pomocny  będzie  rów-
nież schemat z Rysunku 2.

Pierwsze  zadanie  naszego  klien-

ta  to  odczytanie  z  usługi  nazw  wszyst-
kich węzłów, które są dostępne. Milczą-
co  zakładamy,  że  będą  to  tylko  węzły 

calc_node

 – uprości to nasz program.

Po  przygotowaniu  listy  węzłów 

możemy  „rozdać”  poszczególne  wier-
sze obrazu do narysowania. I tu pojawia 
się poważny problem. Wywołania metod 
CORBY zazwyczaj działają w trybie syn-
chronicznym,  czyli  wywołanie  jakiejś 
metody  powoduje  wstrzymanie  pracy 
klienta do czasu, gdy serwer odpowie na 
wywołanie określonej metody. Nie jest to 
dobre rozwiązanie dla naszego klienta, bo 
my chcemy rozdać cały zbiór poszczegól-
nych wierszy do rysowania, a dopiero po 
tej czynności poczekać na gotowe dane, 
aby ostatecznie połączyć wszystkie wier-
sze w jeden obraz. CORBA oferuje odpo-
wiednie rozwiązanie i jest to technika DII, 
o której wspomniałem na początku arty-
kułu.  Dynamiczne  wywołania  pozwala-
ją wywołać zdalną metodę w tzw. trybie 
o  opóźnionej  odpowiedzi.  Praca  progra-
mu  klienta  nie  jest  w  takim  przypadku 
wstrzymywana na czas wykonania zdal-
nej  metody,  ale  to  na  programie  klienta 
spoczywa obowiązek odbioru danych.

Klient  CORBY,  który  stosuje  DII,  nie 

musi  korzystać  z  dodatkowych  plików 
IDL.  Z  tego  powodu  nie  dokonuje-
my  wstępnej  kompilacji  naszego  inter-
fejsu.  Trzeba  jednak  utworzyć  specjalny 
identyfikator,  który  pozwoli  nam  rozpo-
znać  dane  otrzymane  od  metody  serwe-
ra 

render_one_line

 (będzie to oczywiście 

wiersz z kolorami RGB). Musimy utworzyć 
tzw. 

TypeCode

 i wygląda to następująco:

CORBA::TypeCode_ptr _my_tc_longseq;
_my_tc_longseq=CORBA::TypeCode::

S

create_sequence_tc(0, CORBA::_tc_long);

Pozostałe typy danych należą do podsta-
wowych typów CORBY, więc nie trzeba 
stosować  dodatkowych  zabiegów,  aby 
z nimi współpracować. Do pracy nasze-
go klienta potrzebujemy listę wszystkich 
węzłów.  Najlepiej  skorzystać  z  typu 

vector

:

vector<CORBA::Object_var> nodes; 

Potrzebny  jest  jeszcze  jeden  typ,  gdyż 
podczas procesu przydzielania wierszy do 
rysowania będziemy tworzyć listę aktyw-
nych wywołań. Definicja jest następująca:

typedef struct {
   CORBA::Request_var req;
   CORBA::Long y,from_x,to_x;
   int m;
} TRequest; 

Definicja listy, a dokładniej wektora oraz 
iteratora  niezbędnego  do  przeglądania, 
jest następująca:

vector<TRequest> reqs;
vector<TRequest>::iterator reqsIter; 

Pierwsza  z  istotnych  czynności  to  uzy-
skanie  od  usługi  nazw  listy  wszystkich 
węzłów. Fragment z kodu klienta przed-
stawia Listing 5. Listę węzłów odczytuje-
my  wywołaniem 

root_ctx->list(0,  bl, 

bi);

 (zero oznacza, że otrzymamy wszyst-

kie wpisy w usłudze nazw), a następnie 

Listing 3. 

Metoda klasy 

FractalRender

 

rysująca jeden wiersz należący do 
określonego fraktala

def RenderOneLine

(

self, render_y, 

S

                  from_x, to_x

):

# część I

   

self.vWidth

=

self.newvWidth

;

   

self.vHeight

=

self.newvHeight

;

   

self.vfromX

=

self.newvfromX

;

   

self.vfromY

=

self.newvfromY

;

   

self.kx

=

self.vWidth/self.maxx

;

   

self.ky

=

self.vHeight/self.maxy

;

# część II

   

xx

=

self.vfromX 

+ (

from_x 

self.kx

)

   

yy

=

self.vfromY 

+ (

render_y 

S

                     

self.ky

)

   

self.vtoX

=

self.vfromX 

S

                    (

to_x 

self.kx

);

# część III

   

row

=[]

   

while 

(

xx

<=

self.vtoX

):

      

tc

=

iter

(

complex

(

xx,yy

));

      

r

=(

tc 

128

) % 

256

;

      

g

=(

tc 

32

) % 

256

;

      

b

=(

tc 

64

) % 

256

;

      

row.append

(

r

)

      

row.append

(

g

)

      

row.append

(

b

)

      

xx

=

xx

+

self.kx 

   

return 

row 

Listing 4. 

Przykład funkcji iteracji 

odpowiedzialnej za rysowanie zbioru 
Mandelbrota

moja_funkcja

=

”””

def

 iter(c):

   i=

0

   z=complex(

0

,

0

);

   

while

(i < 

128

 and (z.real*z.real 

S

              + z.imag*z.imag) < 

4

):

      z=(z*z)+c
      i=i+

1

  

 return

 i

”””

;

background image

39

www.linux.com.pl

klastry obliczeniowe

corba

w pętli za pomocą 

resolve

 otrzymujemy 

odniesienia  do  obiektów  typu 

CORBA::

Object_var

. Przy okazji w zmiennej 

max_

nodes

 zliczamy ilość dostępnych węzłów. 

Wartość  ta  będzie  nam  potrzebna  pod-
czas dystrybucji poszczególnych wierszy 
do węzłów.

Z  poziomu  funkcji 

main

  wystarczą 

nam  już  tylko  dwa  wywołania  następu-
jących funkcji:

setup_nodes(moja_funkcja, 320, 200);
render_fractal(„rysunek.pnm”, 320, 200); 

Pierwsza  ustali  nam  niezbędne  począt-
kowe parametry, takie jak wymiary oraz 
postać  funkcji  iteracji,  natomiast  druga 
wykona całą „czarną” robotę, czyli nary-
suje  fraktal  oraz  zapisze  go  w  formacie 
PNM  (więcej  informacji  o  tym  formacie 
w ramce Format pliku PNM).

Ustalanie parametrów 

początkowych

Treść funkcji 

setup_nodes

 to tylko pętla 

„przebiegająca”  po  wszystkich  węzłach 
i ustalająca potrzebne parametry. Przed-
stawia się ona następująco:

for(int i=0;i<max_nodes;i++){
   setup_iter_func(nodes[i],

S

 

                   moja_funkcja);
   setup_render_size(nodes[i], w, h);
}

Ponieważ  ustalanie  parametrów  to 
proces łatwy dla poszczególnych serwe-
rów,  więc  nie  ma  potrzeby  stosowania 
wywołania  o  opóźnionej  odpowiedzi 
–  wystarczy  standardowy  tryb  synchro-
niczny.  Treść  funkcji 

setup_render_size

 

to  Listing  6.  Nawet  dla  osób  nie  zna-
jących  CORBY  jest  ona  czytelna.  Para-
metr 

obj

  reprezentuje  zdalny  obiekt 

zapisany  na  liście 

nodes

.  W  funkcji  sto-

sujemy  metodę 

_request

  z  argumentem 

w postaci ciągu znaków reprezentujących 
nazwę  metody,  którą  chcemy  wywołać. 
Następnie  musimy  określić  parametry 
metody.  Mamy  tylko  dwa:  szerokość  (

w

oraz wysokość (

h

). Obiekt 

Request

 posia-

da szereg udogodnień, więc za pomocą 
metody 

add_in_arg

  i  przeciążonego 

operatora 

<<=

 dodajemy dwa parametry. 

Jak  łatwo  wywnioskować,  metodą 

set_

return_type

  określamy  typ  wynikowy 

tej metody i jest to wartość 

void

. Ostatnia 

istotna linia z Listingu 6 to 

req->invoke()

czyli  nakazanie  wywołania  zdalnej 
metody.  W  podobny  sposób  postąpimy 
w przypadku drugiej metody, czyli usta-
lenia treści funkcji ucieczki.

Rysujemy fraktal

Teraz omówimy ostatni element naszego 
systemu – funkcję 

render_fracal

, w której 

następuje  proces  rysowania  fraktala. 
W pierwszej kolejności musimy „rozdać” 
poszczególnym  węzłom  kolejne  wiersze 
do  rysowania.  Poniższa  pętla  jest  spra-
wiedliwa (choć taka strategia nie zapew-
nia  największej  wydajności),  ponieważ 
stara  się  poszczególnym  węzłom  przy-
dzielać równą ilość wierszy (zmienna 

nn

 

zawiera numer aktualnego węzła):

for(rows=0;rows<=h;rows++){
   r.y=rows; r.from_x=0; r.to_x=w; r.m=0;

   reqs.push_back(r); nn++;
   if(nn>max_nodes-1) nn=0;
}

Zmienna 

r

  służy  nam  do  zapamiętania 

wartości  obiektu  wywołania  (

Request

oraz  numeru  wiersza  aktualnie  ryso-
wanego.  Zapamiętujemy  także  wymia-
ry  poziome,  czyli  początek  i  koniec. 
Zawsze  są  to  wartości:  zero  oraz  sze-
rokość  wiersza,  czyli  cały  wiersz.  Treść 
funkcji 

begin_render_one_row

  jest  bliź-

niaczo  podobna  do 

setup_render_size

 

z  Listingu  6.  Różni  się  ostatnią  linią: 

req->send_deferred();

  oraz  tym,  że 

w  wywołaniu 

set_return_type

  jako 

typ  podajemy  własnoręcznie  zdefinio-
wany 

TypeCode

  na  samym  początku 

funkcji 

main

  naszego  klienta.  Metoda 

send_deferred

  zapewnia,  że  klient  po 

jej  wywołaniu  nie  będzie  czekał,  aż 
zostanie  ona  wykonana,  lecz  przejdzie 
do dalszych czynności.

Listing  7  zawiera  ostatnią  już 

pętlę,  sprawdzającą,  czy  żądania
zostały  poprawnie  obsłużone  i  czy
można  odczytać  dane.  Pętla  prze-
gląda  listę  żądań 

reqs

  i  spraw-

dza,  czy  zmienna 

m

  jest  ustawiona 

na zero. Jeśli tak, oznacza to, że żąda-
nie  zostało  już  poprawnie  obsłużone 
i nie trzeba się nim zajmować. W prze-
ciwnym przypadku funkcją 

req_is_end

 

testujemy,  czy  żądanie  zostało  już 
zakończone i jeśli tak, odbieramy dane 
wiersza  wywołaniem 

get_data

,  usta-

wiamy  wartość  flagi 

m

  i  zwiększamy 

zmienną 

i

  (zawiera  ona  ilość  wierszy 

już  narysowanych).  Cała  pętla  zakoń-
czy swoje działanie, gdy liczba wierszy 
będzie  większa  od  wysokości  obrazu 
pomniejszonego o jedność (pamiętajmy, 
że pierwszy wiersz na numer zero).

Sam  proces  testowania,  czy  zada-

nie  zostało  zakończone,  sprowadza  się 
w funkcji 

req_is_end

 do  polecenia: 

req-

>poll_response()

.  Sprawdza  ono,  czy 

żądanie  zostało  obsłużone.  Jeśli  tak  się 
stało, to jak wcześniej podałem, 

get_data

 

Listing 5. 

Uzyskanie listy wszystkich węzłów

root_ctx

->

list

(

0, 

bl, bi

);

max_nodes

=

0

;

while

(

bi

->

next_one

(

b.out

())) 

{

   

CORBA

::

ULong k

;

   

for 

(

0

b

->

binding_name.length 

(); 

k

++)

{

      

name

[

0

].

id

=

b

->

binding_name

[

k

].

id

;

      

name

[

0

].

kind

=

b

->

binding_name

[

k

].

kind

;

      

nodes.push_back

(

root_ctx

->

resolve

(

name

));

      

max_nodes

++;

   

}

}

Listing 6. 

Funkcja ustalająca wymiary rysowanego fraktala na odległym obiekcie

void 

setup_render_size

(

CORBA

::

Object_var obj, CORBA

::

Long 

w, CORBA

::

Long 

h

)

{

   

CORBA

::

Request_var req 

obj

->

_request

(

„setup_render_size”

);

   

req

->

add_in_arg

() <<= 

w

;

   

req

->

add_in_arg

() <<= 

h

;

   

req

->

set_return_type

(

CORBA

::

_tc_void

);

   

req

->

invoke

();

background image

40

maj 2004

klastry obliczeniowe

obiera  dane.  Kod  odbierający  dane  jest 
bardzo krótki:

CORBA::LongSeq row;
req->get_response();
req->return_value() >>= row;
for(int i=0;i<row.length();i++) 
                  *(_row + i) = row[i];

Metoda 

get_response

  kończy  proces 

wywołania  zdalnej  metody  i  pozwala 
„wyciągnąć”  z  metody 

return_value()

 

dane 

reprezentujące 

nasz 

wiersz. 

Następnie  przy  pomocy  prostej  pętli 
przepisujemy  dane  ze  zmiennej 

row

  do 

odpowiedniego miejsca w tablicy

 img

.

Kończąc  opis  klienta,  jeszcze  słowo 

o  tablicy  przechowującej  poszczegól-
ne  wiersze.  Tablica 

img

  to  jednowymia-

rowy  wektor,  w  którym  są  umieszcza-
ne dane wierszy dzięki odpowiedniemu 
adresowaniu.  Adresowanie  może  wyda-
wać się „nieco dziwne”, ale pamiętajmy, 
że pojedynczy piksel jest opisany trzema 
bajtami, więc dlatego podczas adresowa-
nia  współrzędne  musimy  mnożyć  przez 
trójkę.

Jak rysować fraktale?

Zanim  zaczniemy  cokolwiek  rysować 
w  naszym  rozproszonym  systemie, 

należy uruchomić usługę nazw. Zastosu-
jemy usługę nazw dostępną w serwerze 
OmniORB:

omniNames -start -logdir /tmp

Takie polecenie oznacza, że usługa nazw 
założy swój log w katalogu /tmp. Po para-
metrze  -start  opcjonalnie  można  umie-
ści numer portu, na którym działa usługa 
nazw – domyślnie jest to 2809. Jeśli log 
jest  już  założony,  to  wystarczy  podać 
tylko  parametr  z  katalogiem,  gdzie  ten 
log się znajduje:

omniNames -logdir /tmp

Następnym  krokiem  jest  uruchomienie 
serwerów 

calc_node

,  oczywiście  na 

jak  największej  ilości  różnych  kompu-
terów.  Poszczególne  serwery  powin-
ny  oczywiście  posiadać  inną  nazwę: 

./fc_server.py 

wezel_1

.  W  kodzie 

serwera  znajduje  się  także  adres  usługi 
nazw.  W  plikach  znajdujących  się  na 
CD/DVD adres ten odnosi się do kompu-
tera lokalnego, wiec w rzeczywistej sieci 
trzeba go koniecznie zmienić.

Gdy serwery zostały juz uruchomio-

ne  (do  poprawnego  działania  aplikacji 
wystarczy  przynajmniej  jeden),  można 

uruchomić  klienta.  W  jego  przypadku 
także podajemy adres usługi nazw, np.:

ff_clc -ORBInitRef NameService= 

S

corbaloc::127.0.0.1:2809/NameService

Klient  po  zakończonej  pracy  (miejmy 
nadzieję bez kłopotów) powinien pozo-
stawić  po  sobie  plik  o  nazwie  rysu-
nek.pnm
, w którym znajduje się rysunek 
z fraktalem.

Na zakończenie

Łączny  kod  źródłowy  systemu  to  zale-
dwie  10  kB.  Jak  widać,  bardzo  niskim 
nakładem  można  dość  szybko  uzyskać 
rozproszony  system  (a  można  go  nawet 
nazwać  klastrem),  zdolny  do  rysowa-
nia  różnego  typu  fraktali.  Stosując  tego 
rodzaju  podejście  można  bardzo  łatwo 
rozdzielić  każdy  problem,  pod  warun-
kiem, że da się on podzielić na mniejsze 
podproblemy, czyli w przypadku rysowa-
nia fraktali – na poszczególne wiersze.

Co  jeszcze  można  unowocześnić 

w naszym systemie? Oprócz funkcji itera-
cji, warto by było wprowadzić dodatkową 
funkcję kolorującą. Można również wpro-
wadzić znacznie wygodniejszego klienta 
opartego np. o biblioteki QT bądź GTK+ 
–  serwer  MICO  współpracuje  przecież 
z  tymi  bibliotekami  bez  większych  pro-
blemów. Zamiast stosowania oddzielnego 
serwera  CORBY,  można  także  napisać 
klienta  dla  systemu  OmniORB  –  mody-
fikacji  nie  będzie  zbyt  wiele.  Będą  one 
głownie dotyczyć definicji typu wiersza, 
jaki  jest  zwracany  przez  zdalną  metodę 

render_one_row

.

Zachęcam  do  modyfikacji  przedsta-

wionego  systemu,  szczególnie  te  osoby, 
które  interesują  się  technologią  CORBA. 
Na  podstawie  takiego  systemu  można 
zdobyć wiele cennych doświadczeń. 

W Internecie:

–   Strona domowa języka Python:
 

http://www.python.org/

–   Strona projektu OmniORB:
   http://omniorb.sourceforge.net/
–  Strona projektu MICO:
   http://www.mico.org/
–  Strona nieaktualizowana, ale 

zawierająca wiele cennych infor-
macji o fraktalach:

 

http://spanky.triumf.ca/

–   Więcej informacji o fraktalach:
 

http://www.fractalus.com/ifl/

Listing 7. 

Pętla odbierająca wiersze od poszczególnych węzłów

/* cześć I -- odbieranie wyników*/

unsigned char 

*

img

=

new unsigned char

[(

w

+

1

)*(

h

+

1

)*

3

];

vector

<

TRequest

>::

iterator reqsIter

;

i

=

0

;

while

(

< (

h

-

1

))

{

   

reqsIter

=

reqs.begin

();

   

while

(

reqsIter

!=

reqs.end

())

{

      

= *

reqsIter

;

      

if

(

r.m

==

|| 

req_is_end

(

r.req

)==

1

)

{

         

get_data

(

r.req, 

(

img 

+ (

r.y 

3

)));

         

r.m

=

1

;

         

i

++;

      

}

      

reqsIter

++;

   

}

}

/* część II -- zapis danych do pliku*/

   

for

(

x

=

0

;

x

<

w

;

x

++)

{

         

fprintf

(

f_img, 

„%d

\n

”, 

*(

img 

+ ((

y

*

w

*

3

) + (

x

*

3

))));

         

fprintf

(

f_img, 

„%d

\n

”, 

*(

img 

+ ((

y

*

w

*

3

) + (

x

*

3

)+

1

)));

         

fprintf

(

f_img, 

„%d

\n

”, 

*(

img 

+ ((

y

*

w

*

3

) + (

x

*

3

)+

2

)));

      

}

delete 

[] 

img

;