• No results found

Implementace distribuovaného zpracování dat s využitím paralelismu

N/A
N/A
Protected

Academic year: 2022

Share "Implementace distribuovaného zpracování dat s využitím paralelismu"

Copied!
49
0
0

Loading.... (view fulltext now)

Full text

(1)

Technická univerzita v Liberci

Fakulta mechatroniky a mezioborových inženýrských studií

Studijní program: N2612 Elektrotechnika a informatika Studijní obor: 1802T007 Informační technologie

Implementace distribuovaného zpracování dat s využitím paralelismu

Implementation of distributive data processing using parallelism

Diplomová práce

Autor: Bc. Petr Švub

Vedoucí: Doc. Ing. Dalibor Frydrych, Ph.D.

Liberec, 2011

(2)

Zadání

Název projektu: Implementace distribuovaného zpracování dat s využitím paralelismu

Řešitel: Bc. Petr Švub

Vedoucí učitel: Doc. Ing. Dalibor Frydrych, Ph.D.

Zadání:

1. Seznamte se s vytvářením a používáním vzdálených objektů.

2. Seznamte se s technologií synchronizace běhu paralelizovatelných výpočtů.

3. Implementujte funkční model distribuovaného zpracování dat.

4. Model otestujte a porovnejte jeho výkon při různém stupni paralelizace.

(3)

Prohlášení

Byl jsem seznámen s tím, že na mou diplomovou práci se plně vztahuje zákon č. 121/2000 o právu autorském, zejména § 60 (školní dílo).

Beru na vědomí, že TUL má právo na uzavření licenční smlouvy o užití mé DP a prohlašuji, že s o u h l a s í m s případným užitím mé diplomové práce (prodej, zapůjčení apod.).

Jsem si vědom toho, že užít svou diplomovou práci či poskytnout li- cenci k jejímu využití mohu jen se souhlasem TUL, která má právo ode mne požadovat přiměřený příspěvek na úhradu nákladů, vynaložených univerzi- tou na vytvoření díla (až do jejich skutečné výše).

Diplomovou práci jsem vypracoval samostatně s použitím uvedené lite- ratury a na základě konzultací s vedoucím diplomové práce.

Datum

Podpis

(4)

Poděkování

Velmi rád bych poděkoval a vyslovil uznání Doc. Daliboru Frydrychovi, vedoucímu mé diplomové práce, za trpělivé vedení a rozsáhlou podporu při řešení nejrůznějších problémů, na které jsem při tvorbě práce narazil.

Rovněž chci poděkovat celé mé rodině za podporu při studiu jak duševní, tak materiální.

(5)

Obsah

Zadání 2

Obsah 5

Seznam tabulek 6

Seznam obrázků 7

Abstrakt/Abstract 8

Úvod 9

1 Teorie paralelismu 10

1.1 Amdahlův zákon . . . 11

1.2 Technologie jazyka Java . . . 13

2 Analytické modely 19 2.1 Server . . . 19

2.2 Klient . . . 20

2.3 Matice . . . 20

2.4 Načítání a sčítání matic . . . 22

2.5 Násobení matic . . . 24

3 Implementace 31 3.1 Server . . . 31

3.2 Klient . . . 31

3.3 Matrix . . . 33

3.4 Maticové operace . . . 35

4 Testy rychlosti 41 4.1 Algoritmy násobiček . . . 41

4.2 Čtení a zápis do maticových objektů . . . 42

4.3 Načítání matic ze souboru . . . 44

4.4 Paralelizace maticových výpočtů . . . 45

Závěr 48

Literatura 49

(6)

Seznam tabulek

1 Rozšíření třídy java.lang.Thread a přepsání metody run() . 13 2 Implementace rozhraní Runnable a metody run() . . . 14 3 Implementace rozhraní Runnable a metody run() . . . 14 4 Serializace a uložení instance objektu do souboru, a její obnovení 15 5 Vrácení vzdálené instance objektu metodou getRemoteInstan-

ce(String className)(pouze klíčový kód). . . 31 6 Metoda umožňující vkládání prvků do jednoduché matice. . . 34 7 Metoda umožňující vkládání prvků do složené matice. . . 34 8 Inicializační metoda pro CompositeMatrix. . . 35 9 Vytvoření, inicializace a spuštění čtečky pro jednoduchou ma-

tici. Kód metody initReading(String, IMatrix) . . . 36 10 Vytvoření, inicializace a spuštění sčítačky jednoduchých ma-

tic. Kód metody initAdding(IMatrix, IMatrix, IMatrix) 37 11 Sčítání kompozitních matic, kód metody initAdding(IMatrix,

IMatrix, IMatrix) . . . 38 12 Násobení kompozitních matic, kód metody multiply(IMatrix,

IMatrix, IMatrix) třídy MatrixMultiplierManager . . . . 39 13 Násobení kompozitních matic, kód metody multiply(IMatrix,

IMatrix, IMatrix) třídy MatrixMultiplierManager . . . . 40

(7)

Seznam obrázků

1 Srovnání vývoje zrychlení v ideálním případě, dle Amdahlova

zákona a v praxi. . . 12

2 Systém RMI v provozu. . . 18

3 Návrh balíků klienta a serveru. . . 19

4 Návrh balíku pro ukládání matic. . . 21

5 Návrh mechanismu sčítačky. . . 23

6 Násobení v pořadí ijk. . . . 27

7 Násobení v pořadí ikj. . . . 27

8 Násobení v pořadí jik. . . . 28

9 Násobení v pořadí jki. . . . 29

10 Násobení v pořadí kij. . . . 29

11 Násobení v pořadí kji. . . . 30

12 Práce třídy GridComputers. . . 32

13 Porovnání rychlosti jednotlivých typů násobiček. . . 42

14 Porovnání rychlosti čtení matic o rozměrech 400 × 400 prvků. 43 15 Vývoj zrychlení v závislosti na stupni paralelizace úlohy. . . . 44

16 Paralelizace násobení matic. . . 46

(8)

Abstrakt/Abstract

Abstrakt

Diplomová práce je zaměřena na problematiku použití distribuovaného zpra- cování dat na praktickém příkladu. To pak umožňuje nasazení různých stupňů paralelizace, což je rovněž cílem testování.

V úvodu práce je rozebrána teorie paralelismu a Amhahlův zákon. Ná- sleduje stručné seznámení s technologiemi jazyka Java jež byly v práci zkou- mány a nebo použity k řešení. Jedná se o technologie serializace objektů, multithreading a RMI.

Hlavní část práce obsahuje analytický model a dokumentaci implemen- tace distribuovaného zpracování dat, který je použit k testování použitelnosti výše zmíněných technologií. Samotné testy při různé konfiguraci jsou popsány v závěru práce.

Klíčová slova: paralelismus, Java, multithreading, distribuované zpraco- vání dat, vzdálený objekt.

Abstract

The thesis is focused on problematics of distributed data processing and usage of this principle in practice. This could enable use of parallelism. Tests of this principle are included.

First, there is summary of theory of parallelism and Amdahl’s law in- cluded. Followed by brief introduction to Java programming language and its methods, which were used as test subject or used to achieve goal.

Main part of this work contains documentation of system, which is able of distributive data processing. This system is used for testing of Java tech- nologies mentioned above. Tests themselves are included in the final part of thesis.

Keywords: parallelism, Java, Multithreading, distributed data processing, remote object.

(9)

Úvod

Stále výpočetně náročnější problémy v druhé polovině minulého století, jak komerčního, tak vědeckého charakteru, zvyšovaly potřebu výpočetní kapa- city. Dlouhou dobu byly jediným řešením pro skutečně náročné výpočty pouze výkonné superpočítače. Jedním z prvních byl IBM System/360, poprvé představený v roce 1964. Vyroben byl postupně v několika modelech až do roku 1977, kdy byl stažen z trhu. Dalším superpočítačem byl Cray-1, který byl poprvé spuštěn v roce 1976. Podporoval vektorové zpracování instrukcí, tedy paralelismus na velmi jemné úrovni.

Cena superpočítačů byla ale velmi vysoká. Proto takové řešení nebylo zdaleka dostupné pro každého. Rozvoj osobních počítačů a síťových tech- nologií v 80. letech 20. století umožnil později zrod nového přístupu k složitým výpočetním problémům - vznikly distribuované systémy. Distribuované sys- témy dokáží za mnohem nižší cenu nabídnout také poměrně vysoký výkon.

Umožňují rovněž využití paralelismu, ale na mnohem hrubší úrovni. Proto je potřeba za účelem maximálního využití jejich výhod přizpůsobit pro- gramovací techniky.

V současné době existuje mnoho programovacích nástrojů, které umožní vývoj aplikací schopných paralelního zpracování výpočtů s využitím dis- tribuovaného systému. Technologie jazyka Java nabízejí poměrně robustní řešení pomocí tvorby vzdálených objektů. Jedním z cílů práce je ověřit mož- nosti těchto technologií pro případné použití v praxi. Speciálně se jedná o řešení úloh na Ústavu nových technologií a aplikované informatiky na Tech- nické univerzitě v Liberci. Konkrétně vytvoření nadstavby pro výpočetní sys- tém DF2EM, schopné distribuování výpočtů na univerzitní cluster Hydra.

Tato práce se bude zabývat návrhem a implementací výpočetního sys- tému se schopností využívat cluster k paralelnímu zpracování výpočtů. K im- plementaci bude použito programovacího jazyka Java, z důvodu návaznosti na zmíněný výpočetní systém DF2EM. V poslední části práce bude zdoku- mentováno testování systému na vzorových výpočtech spolu se zhodnocením výsledků.

(10)

1 Teorie paralelismu

První vícejádrové procesory pro domácí použití byly na trh uvedeny v polovi- ně roku 2005. V této době už byla patrná stagnace růstu taktovací frekvence procesoru. Mezi lety 1981 a 1995 vzrostl takt z 4,77 MHz na 100 MHz, to je více než dvacetkrát. Od roku 1995 po rok 2002 se takt procesoru dokonce ztřicetinásobil, ze 100 MHz na 3GHz. V současné době činí takt nejrych- lejšího komerčně vyráběného procesoru 5,2 GHz. To není oproti roku 2002 ani dvojnásobek. Přidávání dalších jader se stalo novým způsobem jak efektivně zvyšovat výkon nových modelů.

Tendence k paralelizaci, čili spolupráci menších jednotek, oproti vytváření masivního jednojádrového procesoru jsou pro běžně používanou výpočetní techniku zřetelné. Taktovací frekvence procesoru tedy není tak zásadním ukazatelem výkonu, jako dříve. V některých případech nárůst taktovací frek- vence procesoru dokonce ani příliš nezvýší jeho reálný výpočetní výkon. Krom toho další technologický vývoj naráží za stávajících technologií na čím dál více překážek. Těmi jsou například velikost atomů, maximální možná rychlost šíření signálu a další základní fyzikální principy.

Používání clusterů pro paralelizaci výpočtů se ukázalo jako poměrně efek- tivní řešení. Velkého výkonu je možné dosáhnout, v porovnaní s vektorovými superpočítači, za nepatrnou cenu s běžně dostupnými procesory. I využívání clusterů má však řadu nevýhod. Jednak je třeba zajistit komunikaci mezi jed- notlivými uzly, ale také musí existovat možnost rozdělení daného problému na menší části. Existují bohužel nedělitelné úlohy, jako například výpočet čísla π, kde je k pokračování výpočtu potřeba výsledek předchozí operace. Dalším kritériem je rovnoměrné rozdělení zátěže na dostupný hardware. Špatné roz- dělení může výpočet úlohy naopak ještě zpomalit.

Úrovně paralelismu

Paralelismus jako takový je možné aplikovat na několika funkčních úrovních, které jsou rozlišeny podle granularity. Takzvaná míra granularity se určuje poměrem času stráveného komunikací a času stráveného výpočty [1]. Nej- jemnější úroveň představuje paralelizace jednotlivých příkazů a instrukcí.

Dále dělení pokračuje přes cykly a iterace, vlákna, podprogramy, části úloh a programů, až po nejhrubší úroveň, kdy jsou paralelizovány celé nezávislé úlohy a programy. Z praktických příkladů lze na nejjemnější úroveň pa- ralelismu zařadit pipelining, to je zřetězené zpracování nebo překrývání in- strukcí. Základní myšlenkou pipeliningu je rozdělení strojové instrukce na nezávislé kroky. Každému kroku je pak přidělena vlastní výpočetní jednotka.

Poměrně jemná granularita je dosahována pomocí skalárních a vektorových

(11)

CPU. S nejhrubší úrovní paralelizace je možné se setkat u počítačových clus- terů.

1.1 Amdahlův zákon

V roce 1967 formuloval Gene Myron Amdahl vztahy pro zrychlení výpočtu, dosažitelného paralelizací. Tyto vztahy, známé jako Amdahlův zákon, lze použít k výpočtu předpokládaného zrychlení, které získáme paralelizací algo- ritmu. Amdahlův zákon vychází z předpokladu, že každá aplikace má určitou část, kterou nelze paralelizovat a tudíž je nutné ji provést sekvenčně.

Dále v textu budou použita tato označení:

Tc - celkový čas potřebný pro zpracování úlohy na jednom procesoru

Tp - čas potřebný pro zpracování části úlohy, kterou lze rozdělit na nezávislé dílčí úlohy

Ts - čas potřebný pro zpracování části úlohy, kterou je nutné zpracovat sekvenčně

Z definice plyne že Tc = Tp + Ts. Předpokládejme, že paralelizovatelnou část lze rozdělit na k stejně velkých a nezávislých částí a že máme k dispozici alespoň k procesorů. Pak mohou být všechny podúlohy zpracovány současně a celá úloha bude zpracována v čase T (k), který lze určit vztahem

T (k) = Ts+Tp

k (1)

Jedním z nejdůležitějších parametrů, které udávají zvýšení výkonnosti získané paralelizací, je zrychlení, dané vztahem (2).

Označme

p - poměr paralelizovatelné části úlohy s - poměr sekvenční části úlohy

zrychlení pak vypočteme vztahem S(k) = s + p

s + pk = 1

s + pk (2)

Jednoduše lze dokázat, že pro zrychlení platí S(1) = 1 a 1 ≤ S(k) ≤ k.

Nicméně to nejdůležitější, co lze z výše uvedeného vztahu odvodit, je: i když

(12)

Obrázek 1: Srovnání vývoje zrychlení v ideálním případě, dle Amdahlova zákona a v praxi.

použijeme sebevětší počet procesorů, nikdy nedosáhneme většího zrychlení než 1−p1 . Pokud bude nutné provést například pětinu výpočtu sekvenčně (p = 0.8), lze teoreticky dosáhnout zrychlení nejvýše pět.

Porovnání křivek zrychlení S pro praxi, Amdahlův zákon a ideální pří- pad je zachyceno na Obrázku 1. Obrázek ukazuje různé závislosti velikosti zrychlení na počtu procesorů. V ideálním případě by mělo se zvyšujícím se počtem procesorů rovnoměrně narůstat i zrychlení, a tato přímka je vyz- načena zelenou barvou. Modrou barvou je vyznačena křivka zrychlení S pro Admahlův zákon. Horní hranice je zde stanovena poměrem paralelizovatelné části výpočtu - znázorněna světlou přerušovanou čarou. Křivka průběhu zrychlení v praxi je vyznačena červenou barvou. Její klesající tendence při čím dál větším počtu procesorů je způsobena nedokonalostí technologií, síťovou režií, a podobně. Každý další přidaný procesor zvýší hodnotu zrychlení o něco méně. Režie, spojená s přidáním dalšího procesoru, nakonec převáží nad pří- růstkem výkonu a hodnota zrychlení bude od určitého bodu s přidáváním dalších procesorů klesat.

Amdahlův zákon také poukazuje na to, že samotné přídavné paralelní náklady je nutno uvažovat jako sekvenční část úlohy. Tím je maximální dosažitelné zrychlení omezeno na hodnoty pět až sedm [2].

(13)

1.2 Technologie jazyka Java

Jazyk Java nabízí různé efektivní nástroje pro paralelní programování. Díky rozsáhlé komunitě jde vývoj nových knihoven a jejich způsobů použití neustále kupředu. K dispozici je samozřejmě také přehledná dokumentace. API jazyka Java odstiňuje programátora od mnoha problémů, a ten tak může veškerou svou pozornost věnovat dosažení konečného cíle. Možnosti jayzka Java je ale třeba správně využít.

Multivláknové zpracování procesů

V kontextu programovacího jazyka Java znamená slovo vlákno (thread) dvě věci. Jednak je to instance třídy java.lang.Thread, což je klasický objekt se svými proměnnými a metodami, a jeho existence je omezena na datovou strukturu zvanou heap nebo také halda. Ve druhém případě slovo vlákno představuje samostatný odlehčený proces, který ma vlastní call stack (zá- sobník volání). Virtuální stroj Javy (JVM - Java Virtual Machine) vytváří vlákna i nezávisle na programátorovi. Například metoda main(), která umož- ňuje spuštění Java aplikace, je spouštěna ve vlákně jménem main.

Vlákno v Javě začíná jako instance třídy java.lang.Thread. Třída imple- mentuje operace s vlákny zahrnující vytváření, spouštění, uspávání, odstavení nebo například kontrolu dokončení běhu vlákna. Konkrétní práce, kterou má vlákno za svého běhu vykonat, je v metodě run. Všechen kód, který je potřeba vykonat v samostatném vlákně, musí být implementován právě zde.

K definování a vytvoření instance vlákna existují dvě cesty, rozšíření třídy java.lang.Thread, a nebo implementace rozhraní Runnable.

Tabulka 1: Rozšíření třídy java.lang.Thread a přepsání metody run()

class MojeVlakno extends Thread { public void run() {

System.out.println("Kod, vykonany za behu vlakna");

} }

Rozšíření třídy java.lang.Thread a přepsání metody run() (viz Ta- bulka 1) s sebou přináší jedno poměrně zásadní omezení. Pokud třída rozšíří třídu Thread, nemůže pak rozšířit žádnou další třídu. Java neumožňuje ví- cenásobnou dědičnost. Vlastnoti třídy Thread není potřeba získat děděním, protože ve výsledku je vždy vytvořena její instance. Takže to tento krok neospravedlňuje. Metodu run() je možné jakkoliv přetížit (znovu implemen- tovat se stejným jménem, ale jinými vstupními argumenty), taková metoda bude však třídou Thread ignorována a jediným způsobem jak provést její kód

(14)

je zavolat ji explicitně. Vytvoření instance vlákna je v tomto případě velmi jednoduché: MojeVlakno t = new MojeVlakno().

Tabulka 2: Implementace rozhraní Runnable a metody run()

class MojeVlakno implements Runnable { public void run() {

System.out.println("Kod, vykonany za behu vlakna");

} }

Implementace rozhraní Runnable umožňuje rozšířit jakoukoliv třídu, a stále definuje funkčnost, která bude vykonávána v odděleném vláknu (pří- klad v Tabulce 2). Vytvoření instance vlákna je jen mírně složitější. Nejprve je vytvořena instance třídy, která rozhraní Runnable implementuje:

MojeVlakno r = new MojeVlakno();

V dalším kroku je vytvořena instance vlákna:

Thread t = new Thread(r);

Po vytvoření instance třídy Thread je vlákno ve stavu nové (new). Aby se kód, implementovaný v příslušné metodě run(), mohl začít provádět ve vlákně, je potřeba na instanci vlákna zavolat spouštěcí metodu t.start().

Po tomto kroku startuje skutečné vlákno s vlastním zásobníkem volání (call stack), vlákno je přesunuto ze stavu nové do stavu běhuschopné (runnable) a po přidělení systémových prostředků započne vykonávání kódu metody run(). Po dokončení metody run() je vlákno přesunuto do stavu mrtvé (dead). Pokud se na konkrétní instanci vlákna již volala spouštěcí metoda start(), vlákno za běhu ani po jeho skončení nelze spustit touto metodou znovu. Kompletní příklad definování vlákna, vytvoření instance a spuštění je v Tabulce 3.

Tabulka 3: Implementace rozhraní Runnable a metody run()

class MojeVlakno implements Runnable { public void run() {

System.out.println("Vlakno bezi");

} }

public class VlaknaTest {

public static void main (String [] args) { MojeVlakno r = new MojeVlakno();

Thread t = new Thread(r);

t.start();

} }

(15)

V praxi je běžnější použití impelementace rozhraní Runnable. Rozšíření třídy Thread je poněkud snadnější, ale ve většině případů neodpovídá zása- dám objektově orientovaného programování. Podle nich by potomek měl být specializovanou verzí obecněji zaměřeného předka. V tomto případě by se tedy mělo jednat o specializovanou verzi třídy java.lang.Thread se speci- fickým chováním (dle zásad OOP). Většinou je však pravděpodobné, že vlá- kno bude vytvořeno za účelem vykonání nějaké konkrétní činnosti, a tak budou jeho vlastnoti pouze využity, nikoli upraveny.

Serializace objektů

Je-li potřeba uložit stav nějakého objektu, můžeme pro to použít technologii jazyka Java zvanou serializace. Serializace umožní uložit objekt a jeho in- stanční proměnné, které nejsou označeny klíčovým slovem transient, po- mocí metody ObjectOutputStream.writeObject(). Deserializace a načtení objektu je provedeno pomocí metody ObjectInputStream.readObject().

Třídy java.io.ObjectOutputStream a java.io.ObjectInputStream fun- gují tak, že se do nich takzvaně zabalí třídy nižší úrovně, jako například java.io.FileOutputStream a java.io.FileInputStream. V následujícím jednoduchém příkladu (viz Tabulka 4) je program, který vytvoří objekt typu Matice, serializuje ho a následně deserializuje:

Tabulka 4: Serializace a uložení instance objektu do souboru, a její obnovení

import java.io.*;

class Matice implements Serializable { } public class SerializeMatice {

public static void main(String[] args) { Matice o = new Matice();

try {

FileOutputStream fos = new FileOutputStream("serializace.out");

ObjectOutputStream oos = new ObjectOutputStream(fos);

oos.writeObject(o);

oos.close();

} catch (Exception e) { e.printStackTrace(); } try {

FileInputStream fis = new FileInputStream("serializace.out");

ObjectInputStream ois = new ObjectInputStream(fis);

o = (Matice) ois.readObject();

ois.close();

} catch (Exception e) { e.printStackTrace(); } }

}

Za povšimnutí stojí, že třída Matice implementuje rozhraní Serializa- ble. Toto rozhraní nemá deklarovány žádné metody, které by bylo potřeba implementovat, slouží pouze pro označení. Dále v kódu příkladu je vytvořena

(16)

instance třídy Matice, o které víme, že je serializovatelná. Následuje samotná serializace, která ovšem vyžaduje určité přípravy. Nejprve je třeba všechny I/O operace umístit do chráněného bloku try-catch, pak je potřeba vytvořit FileOutputStream, do kterého bude objekt zapsán. Konečně je FileOutput- Stream zabalen do objektu typu ObjectOutputStream, který samotnou se- rializaci realizuje, a voláním jeho metody writeObject() je instance třídy Matice serializována a zároveň zapsána do souboru ”serializace.out”. Dese- rializace probíhá analogicky, volání metody readObject() vrátí objekt typu Object, a proto je nutné ho přetypovat na správný typ.

Jestliže jsou všechny instanční proměnné serializované třídy primitivní datové typy, je serializace poměrně jednoduchá. Komplikovanější je seriali- zace tříd, jejichž instanční proměnné jsou samy reference na jiné objekty.

Nemá smysl ukládat aktuální hodnoty těchto referencí, protože hodnota re- ferenční proměnné ma v Javě smysl pouze v omezeném kontextu. Na jiném stroji, nebo i na stejném stroji ale v jiné instanci Java Virtual Machine (JVM) by byla hodnota referenční proměnné nepoužitelná. Při serializaci ob- jektu, který obsahuje reference na jiné objekty jsou tedy automaticky seriali- zovány i tyto, a pokud samy obsahují reference na jiné objekty, také budou do serializace zahrnuty, a tak dále. Jestliže ale třída serializovaného objektu neimplementuje rozhraní Serializable, program po spuštění vrátí výjimku java.io.NotSerializableException. Implementaci rozhraní je možné pro další pokus o serializaci doplnit, pokud ale programátor nemá k dané třídě přístup, nezbývá než proměnnou, která referenci na danou třídu obsahuje, vyloučit ze serializace použitím klíčového slova transient. Po deserializaci takové proměnné obsahují prázdnou hodnotu null. Tomu je možné zamezit implementací metod private void writeObject(ObjectOutputStream s) a private void readObject(ObjectInputStream s) do třídy, kterou bu- deme serializovat, a víme, že obsahuje referenci na neserializovatelný objekt.

V těchto metodách je možné zahrnout kód, který se automaticky provede při serializaci i deserializaci. Například uložení hodnot primitivních datových typů neserializovatelného objektu, na který je odkazováno, do streamu, a je- jich opětovné obnovení při deserializaci.

Pokud třída impementuje rozhraní Serializable, schopnost serializace se přenese i na její potomky. O tříde lze s určitostí tvrdit, že není seriali- zovatelná, pokud explicitně nerozšiřuje žádnou jinou třídu, ani neimplemen- tuje rozhraní Serializable. Třída Object, předek všech tříd v Javě, rozhraní Serializable neimplementuje. Při pokusu o serializaci serializovatelné třídy, jež je potomkem neserializovatelné třídy, proběhne serializace v pořádku. Při deserializaci je však volán konstruktor neserializovatelné třídy (a konstruk- tory všech předků této třídy), a jestliže v něm probíhá nějaká inicializace proměnných, tyto proměnné budou v deserializovaném objektu změněny.

(17)

Pokud třída obsahuje kromě instančních proměnných také statické proměnné, označené klíčovým slovem static, serializace jejich hodnoty nezachová. Hod- noty statických proměnných jsou spjaty s kontextem celé třídy, a ne s kon- textem její konkrétní instance.

Technologie RMI

Technologie Java RMI, Remote Method Invocation, umožňuje vytvářet a po- užívat takzvané vzdálené objekty. To dává programátorovi jedinečnou mož- nost využívat poměrně jednoduše v rámci jednoho programu více výpočetních zdrojů. Vzdálený objekt navenek vypadá jako lokální, ale jeho kód může být vykonán na jiném počítači.

Aplikace, vytvořené pomocí technologie RMI, se obvykle skládají ze dvou částí, serveru a klienta. Obě části mohou, ale nemusí běžet na stejné JVM (Java Virtual Machine) a rozdílné JVM mohou být případně spuštěny každá na jiném stroji. Server vytváří vzdálené objekty a zprostředkuje klientovi vzdálené reference na ně. Klient může volat metody těchto vzdálených ob- jektů, přičemž je využíván výpočetní výkon stroje, na kterém je spuštěn server - a tudíž kde je vytvořena vzdálená instance objektu. RMI dodává mechanismus, pomocí kterého mezi sebou klient a server komunikují a předá- vají si data. RMI aplikace se nazývají také distribuované aplikace.

Objekt se stane vzdáleným pouze pokud implementuje vzdálené rozhraní.

To je takové rozhraní, které rozšiřuje (extends) rozhraní java.rmi.Remote a zároveň každá metoda deklarovaná ve vzdáleném rozhraní vrací výjimku java.rmi.RemoteException pomocí klauzule throws. Vzdálený objekt by také měl rozšiřovat třídu java.rmi.server.UnicastRemoteObject. Vlast- nosti třídy umožní export vzdáleného objektu do adresního prostoru serveru tak, aby byl pro klienta dosažitelný. Ke vzdáleným objektům se RMI při přenosu mezi JVM chová odlišně oproti bežným objektům. Namísto zkopíro- vání celé implementace objektu tak, aby byla dostupná i ve vzdálené JVM, předává RMI takzvaný stub pro daný vzdálený objekt. Stub, nebo také zás- tupný objekt, se chová jako lokální reference na vzdálený objekt. Klient volá metody stubu, který je odpověný za vzdálené volání metody příslušného vzdáleného objektu na serveru. Stub má samozřejmě všechny vlastnosti, které má konkrétní vzdálený objekt, a klient, vlastník stubu, je může plně využí- vat. RMI při komunikaci mezi klientem a serverem používá marshalling argu- mentů. Marshalling je proces převodu informace do formátu, který je vhodný k přenosu, a její opětovné získání a použití například jako argumentu při volání metody. Tady je zřejmá určitá režie, která tuto operaci činí časově mnohonásobně náročnější oproti bežnému, lokálnímu volání metod.

(18)

Obrázek 2: Systém RMI v provozu.

Provoz systému (viz Obrázek 2) začíná spuštěním programu serveru.

Server vytvoří instanci objektu serveru a zaregistruje jeho stub do rmiregis- try, aby byl přístupný klientovi. Děje se tak voláním metod bind(), respek- tive rebind() třídy java.rmi.Naming. Obě metody spustí vlákno s vazbou na instanci objektu serveru, které bude obsluhovat požadavky. Aby klientský program mohl volat metody vzdáleného objektu, musí vyhledat a získat jeho stub prostřednictvím statické metody lookup() třídy java.rmi.Na- ming. Instance je vytvořena voláním konstruktoru vzdáleného objektu. Po- tom lze získat referenci typu java.rmi.Remote, kterou je třeba přetypovat na patřičný typ.

(19)

2 Analytické modely

Analytická část práce se zabývá návrhem aplikace a rozborem použití tech- nologií, popsaných v kapitole 1.2, při zpracování maticových výpočtů. Aby bylo možné tyto technologie, především technologii RMI, efektivně použít při řešení skutečných vědeckých problémů, je potřeba nejprve otestovat je- jich výkon a efektivitu v různých situacích. Proto bude navrženo několik sad testovacích scénářů, které otestují možnost nasazení RMI při řešení praktick- ých problémů. Pro grafické znázornění modelů aplikace bylo použito mode- lovacího jazyka UML (Unified Modeling Language) [3]. UML class diagramy přehledně znázornňují vztahy mezi třídami. Pro návrh některých komponent aplikace bylo použito návrhových vzorů [4].

2.1 Server

Část aplikace, označovaná jako server, představuje RMI server. Je schopen vytvářet instance vzdálených objektů, a poskytovat klientovi reference na ně.

Třída RMIRemoteFactory (viz Obrázek 3) reprezentuje samotný server a na každém hostitelském stroji, který bude využíván k výpočtům, musí bežet ale- spoň jedna jeho instance. V metodě main(), která je volána po spuštění třídy, je zavolán konstruktor RMIRemoteFactory. V tuto chvíli je server spuštěn a připraven plnit požadavky klienta. Hlavním úkolem serveru je vytvářet instance vzdálených objektů na hostitelském stroji a vracet reference na ně.

Obrázek 3: Návrh balíků klienta a serveru.

Třída Environment, která je také součástí balíku serveru, umožňuje po- mocí několika různých metod identifikovat hostitelský stroj vzdáleného ob- jektu.

(20)

2.2 Klient

Klienstká strana aplikace se skládá z mnoha balíků a tříd, rozdělených podle typu testů. Při návrhu je kladen důraz na co největší odstínění od konkrétní implementace, proto klient pracuje především s rozhraními. Každá sada testů se soustředí na nějakou část maticových operací, například vytváření vzdále- ných matic pomocí RMI serveru, čtení a zápis do matic, sčítání a násobení.

Ověřuje jejich správnou funkčnost a porovnává rychlost operací při různém stupni paralelizace. Práci konkrétních tříd klienta usnadňují, a hlavně odsti- ňují od serveru, pomocné třídy ServerConnector a GridComputers.

Třída ServerConnector (viz Obrázek 3) umožňuje klientské straně ap- likace získat připojení k serveru. Po získání reference na konkrétní běžící RMIRemoteFactory je klient schopen vznést požadavek na vytvoření instance vzdáleného objektu. Třída ServerConnector je vytvořena podle návrhového vzoru Singleton. Má soukromý konstruktor a veřejnou metodu getServer- Connector(), která vrací referenci na jedinou instanci této třídy.

Část programu, označovaná jako grid, představuje rozhraní mezi serverem a klientem. Jeho úkolem je zjednodušit přístup klienta k funkcím serveru a odstínit tyto dvě komponenty v duchu zásad objektově orientovaného pro- gramování. Klient tedy pohodlněji využívá funkcí serveru a zároveň nepotře- buje žádné informace o jeho konkrétní implemetaci. Třída GridComputers udržuje aktuální seznam běžících serverů a zprostředkuje klientovi metody komponenty server. Poskytuje metody pro vytvoření instance vzdáleného ob- jektu a získání reference na ni, a pro získání reference přímo na instanci RMIRemoteFactory, pomocí metod třídy ServerConnector. Je také vytvořena dle návrhového vzoru Singleton.

2.3 Matice

Na matice se program soustředí z důvodu možnosti snadného rozdělení mati- cového výpočtu na dílčí ulohy a možnosti jeho paralelizace. K operacím s ma- ticemi také ústí řada vědeckých problémů.

K uložení jednotlivých prvků matice bylo zvoleno dvojrozměrné pole proměnných typu double. Pole bude atributem třídy, která reprezentuje matici a zároveň implementuje základní maticové operace. Pro reprezentaci matic byly navrženy dvě třídy, Matrix a CompositeMatrix. Obě třídy rozši- řují abstraktní třídu AbstractMatrix, která rozšiřuje java.rmi.server.Uni- castRemoteObject, implementuje metodu getHostName() a implementuje rozhraní IMatrix. Voláním metody getHostName(), kterou dědí obě mati- cové třídy, je možné zjistit (za použití třídy Environment z balíku serveru) hostitelský stroj, na němž byl vzdálený objekt dané matice vytvořen. Třídy

(21)

Obrázek 4: Návrh balíku pro ukládání matic.

Matrix a CompositeMatrix implementují metody rozhraní IMatrix, protože jsou přímými a neabstraktními potomky abstraktní třídy AbstractMatrix, která rozhraní implementuje. Třída CompositeMatrix implementuje navíc rozhraní ICompositeMatrix, které deklaruje další metody pro práci s ma- ticemi typu CompositeMatrix. Rozhraní IMatrix deklaruje několik metod čtení a zápisu jednotlivých prvků, řádků, sloupců i celého maticového pole najednou. Dále deklaruje metody pro inicializaci matic s různými vstupními argumenty, a metody pomocí nichž je možné zjistit některé vlastnosti matic.

Obě rozhraní rozšiřují rozhraní java.rmi.Remote.

Třída Matrix reprezentuje jednoduché matice. Všechny prvky jedné celé matice jsou uloženy v dvojrozměrném poli, které je atributem třídy. Metody, impementované třídou Matrix, umožňují provádět některé důležité operace.

Inicializace je možná vložením existujícího pole, pole náhodně vygenerovaných prvků nebo vytvořením nulové matice o zadaných rozměrech. Metody pro přístup k prvkům vrací nebo mění specifikovanou část pole, ať jde o prvek, řádek, sloupec nebo celé pole. Další metody slouží k určení počtu sloupců a řádků matice, uložené v objektu.

Kompozitní matice reprezentuje třída CompositeMatrix. Jak název třídy napovídá, matice, která je v objektu třídy uložena, je rozdělena na několik částí. Namísto dvojrozměrného pole typu double je hlavním atributem třídy dvojrozměrné pole typu IMatrix. Kompozitní matice drží pomocí tohoto pole reference na několik jednoduchých matic a organizuje je tak, že s nimi lze pra- covat jako s jednou celistvou maticí. V praxi by mělo být možné díky tomuto

(22)

uspořádání matici rozprostřít na několik hostitelských strojů zároveň. Třída CompositeMatrix implementuje stejné metody jako třída Matrix a navíc několik metod deklarovaných v rozhraní ICompositeMatrix. Jsou to metody pro zjištění počtu maticových segmentů, tedy počet jednoduchých matic uložených ve sloupcích a řádcích. Dále speciální inicializační metody pro stanovení počtu kompozitních segmentů a metody pro vkládání celých matic na patřičné souřadnice. Jednoduchou matici lze vložit tak, aby byla rozdělena na segmenty při vložení. Metoda pro získání konkrétní jednoduché matice z určené souřadnice je také zahrnuta.

Rozhraní IMatrixType pomáhá určit, jedná-li se o instanci třídy Matrix, nebo CompositeMatrix, k oběma bude přistupováno jako k typu IMatrix.

Volání příslušné metody maticových tříd vrátí konstantu, jejíž hodnota je definována právě zde.

2.4 Načítání a sčítání matic

Operacemi sčítání matic a načítání matic ze souboru se zabývají maticová sčítačka a čtečka z balíků adder a reader.

Načítání matic ze souboru

Komponenta, zabývající se načítáním matic ze souboru, nabízí několik způ- sobů, jak operaci provést. Načítání lze uskutečnit centrálně, kdy jediná in- stance čtečky MatrixReader přečte všechna čísla uložená v souboru a uloží je jako prvky do jediné matice. Na typu matice přitom nezáleží, vkládání probíhá stejným způsobem pro jednoduché i pro kompozitní matice. Druhý způsob realizuje manažer načítání, který bude implementovaný ve třídě Ma- trixReaderManager. Ten pro každý blok matice na jeho hostitelském stroji vytvoří čtečku, která do bloku načte příslušnou část souboru. Pro každý blok kompozitní matice tak beží vlastní čtečka ve vláknu, a jestliže jsou bloky rozdistribuovány na několik hostitelských strojů, načítání probíhá para- lelně. Třídy, které implementují načítací algoritmy, rozšiřují abstraktní třídu AbstractMatrixReader, která rozšiřuje třídu UnicastRemoteObject a im- plementuje rozhraní IMatrixReader. Rozhraní IMatrixReader deklaruje me- todu pro spuštění načítání, jejíž vstupní argumenty jsou reference na matici a název souboru.

Sčítání matic

Operace sčítání matic (viz Obrázek 5) probíhá plně v režii manažera sčítání.

Klient zavolá metodu, jíž jako argumenty předá reference na tři matice. Dvě

(23)

z matic jsou určeny k sečtení a třetí k uložení výsledku. Manažer nejprve provede testy ohledně korektnosti zadaných matic - pouze matice o stejných rozměrech lze sčítat. Zjistí, jedná-li se o matice jednoduché, nebo kompozitní.

Dále u každé matice, případně u každého segmentu, manažer identifikuje hos- titelský stroj, na kterém se matice nachází. Informace vyhodnotí a, s ohledem na co možná největší efektivitu, vytvoří konkrétní sčítačky. Na obrázku 5 je hostitelský stroj sčítaček označen jako PC X, protože o tom, kde budou sčí- tačky založeny, rozhodne manažer až po vyhodnocení situace. Během operace sčítání by pak mělo docházet k co nejmenší komunikaci mezi jednotlivými stroji v síti. Pokud se například dvě z matic nachází na stroji A a třetí matice má jako hosta stroj B, manažer vytvoří sčítačku na stroji A, aby bylo možno prvky dvou matic přistupovat lokálně a pouze pro přístup k prvkům třetí matice využívat síť. Při vytváření sčítaček předá manažer každé z nich refe- rence na matice, případně segmenty matic, které mají být pro danou operaci sčítání použity. Jakmile je vše připraveno, manažer sčítání spustí a předá reference na běžící vlákna sčítaček zpět klientovi. Ten po kontrole skončení běhu nalezne v příslušné matici výsledek sčítání.

Obrázek 5: Návrh mechanismu sčítačky.

Manažera sčítání je třeba navrhnout tak, aby, s ohledem na situaci, za- kládal sčítačky za účelem minimalizace síťové komunikace mezi jednotlivými stroji. Z tohoto pohledu mohou typicky nastat tři různé situace, podrobněji rozebrané níže, které musí manažer řešit individuálně.

1. Všechny matice na stejném hostu. Sčítačka je vytvořena tam, a komu- nikace s jinými stroji v síti není potřeba. Tento způsob je z pohledu síťové režie nejvýhodnější.

(24)

2. Dvě z matic jsou vytvořeny jako vzdálené objekty na stejném hosti- telském stroji, třetí matice je umístěna jinde. Sčítačka je vytvořena na společném hostu dvou matic a do obou přistupuje lokálně. Pouze do poslední matice je nucena přistupovat pomocí sítě.

3. Každá matice má vlastní hostitelský stroj. Toto je zřejmě nejméně výhodná varianta. Sčítačka je vytvořena na hostu jedné z matic a do ostatních dvou je nucena přistupovat pomocí síťové komunikace. Síťová režie tak vzroste.

Další možností je navrhnout takovou sčítačku, která si od každé matice určené k sečtení vyžádá celé maticové pole najednou (metoda getArray()).

Do výsledné matice po skončení operace vloží celé pole opět najednou (me- toda putArray(dounle[][])). V takovém případě by náklady na síťovou ko- munikaci mohly být teoreticky menší. Na umístění sčítaček v clusteru vzhle- dem k maticím pak nebude nutné brát ohled.

2.5 Násobení matic

Maticové násobení má v programu na starosti komponenta nazvaná manažer násobení, reprezentovaná třídou MatrixMultiplierManager z balíku mul- tiplication. Manažer násobení provádí svou funkci na podobném principu jako manažer sčítání. Od klienta převezme reference na tři matice, dvě určené k vynásobení a jedna k uložení výsledku. Nejprve zjistí rozměry matic a zda je možné matice násobit, dále jedná-li se o matice jednoduché, nebo kompo- zitní. Pokud jde o matice jednoduché, zjistí hosta každé z nich a podle toho je založena optimální násobička analogicky jako v případě maticového sčítání.

V případě kompozitních matic je třeba segmenty násobit dle jistého vzorce.

Určité bloky vynásobené mezi sebou dají blok dílčích výsledků, který je třeba sečíst s výsledkem násobení jiných dvou bloků, a tak dále, dokud manažer ne- projde všechny bloky v daném řádku první matice, a všechny bloky v daném sloupci druhé matice. Jednou možností je projít všechny bloky postupně a nechat výsledek přičítat do připravených výsledných bloků. Pro každé dva bloky, určené k vynásobení, a jeden, určený k uložení výsledku manažer zjistí hosty bloků a v závisloti na tom založí optimální násobičku. Ihned potom spustí vlákno násobení a pokračuje dále, dokud všechna vlákna neběží. Re- ference na vlákna udržuje v dvojrozměrném poli, které potom vrátí klientovi.

Po skončení běhu všech vláken nalezne klient v příslušné matici výsledek ná- sobení. Další možností je použití Cannonova algoritmu pro násobení matic rozdělených na segmenty.

(25)

Cannonův algoritmus

Cannonův algoritmus byl poprvé popsán v roce 1969 a jeho autorem je Lynn Elliot Cannon. Operace násobení matic A a B je rozdělena po p2 dílčích pro- cesech, spouštených postupně v p vlnách. Matice A a matice B jsou rozděleny každá na p× p čtvercových bloků a násobení potom probíhá tak, že má každý proces v danou chvíli k dispozici právě jeden blok z matice A a právě jeden blok z matice B. Přitom platí, že žádný z procesů v danou chvíli nepracuje s blokem, s kterým by pracoval i jiný proces. Má přidělen takový blok z obou násobených matic, ke kterému přistupuje jen on. Algoritmus si tak udržuje konstantní paměťovou náročnost.

Funkce algoritmu je následující: v prvním, inicializačním kroku algoritmus přerovná bloky v maticích A a B tak, že v matici A posune všechny bloky na stejném řádku doleva o i pozic, kde i je číslo řádku, počítáno od nuly.

V matici B pak posune všechny bloky ve stejném sloupci nahoru o j pozic, kde j je číslo sloupce, počítáno od nuly. Bloky které přesáhnou rámec ma- tice zařadí na počáteční pozici daného sloupce, nebo řádku. Nyní lze bloky na stejných souřadnicích v obou maticích mezi sebou vynásobit, a výsledné bloky takových násobků uložit například do připravené matice C. Další kroky už probíhají podle stejného vzoru: bloky v matici A se pro další krok posunou všechny o jednu pozici doleva. Bloky v matici B jsou posunuty o jednu pozici směrem vzhůru. Po každém posunu bloků proběhne násobení bloků na stej- ných pozicích v maticích A a B, a výsledné bloky jsou přičteny na příslušné pozice v matici C. Jakmile jsou pozice bloků v maticích stejné jako po ini- cializačním kroku, algoritmus dále nepokračuje.

Algoritmy maticového násobení

Určení složitosti algoritmu se provádí posouzením závislosti doby výpočtu na objemu vstupních dat. Pro větší objektivnost metody se používá porov- návání asymptotických složitostí. Složitost algoritmu maticového násobení je klasifikována jako kubická. To znamená, že pro n vstupních prvků bude vykonáno n3 operací. Maticové násobení se mezi ostatními maticovými ope- racemi jeví jako náročnější algoritmus a vzhledem k tomu má určitě smysl zabývat se jeho optimalizací.

Standardně implementovaný algoritmus maticového násobení obsahuje tři do sebe vnořené cykly. Vzhledem ke struktuře vnořených cyklů existují tři úrovně iterace a tři úrovně přístupu k maticím. Pro jeden prvek z nějaké ma- tice je přistupován v nejvnitřnějším cyklu celý vektor druhé matice a všechny prvky zbývající matice. V prostředním cyklu je iterována proměnná určující pozici prvku, ve vnějším cyklu je iterována druhá souřadnice prvku a souřad-

(26)

nice, ze které je načten vektor. Je patrné, že zatímco v jedné matici je pro jednu iteraci vnějšího cyklu přistupován pouze jeden prvek, v druhé matici je vektor přistupován opakovaně celý a v třetí matici jsou postupně přis- tupovány všechny prvky. V programovacích jazycích hraje z hlediska rychlosti roli jak přístup do lokální a globální proměnné, tak struktura proměnné, v níž jsou data uložena. Prvek v dvojrozměrném poli uloženém jako globální proměnná bude zřejmě přistupován pomaleji, než primitivní jednoprvková proměnná, uložená lokálně. Při návrhu algoritmů maticového násobení je tedy výhodné vzít tato fakta v úvahu a návrh jim vhodně přizpůsobit. Na začátku vnějšího cyklu je možné načíst do pomocné lokální proměnné vektor, který bude v dalších dvou cyklech opakovaně čten. Během středního cyklu je pak vhodné načíst do lokální proměnné prvek, který bude opakovaně čten ve vnitřním cyklu. Pořadí tří do sebe vnořených cyklů je navíc možné obměňo- vat.

Aby bylo možné násobit matice A, B a C, kde matice C je určena pro uložení výsledku, musí mít matice rozměry A : i×j, B : j×k a C : i×k. Cykly for probíhají od 0 až po konečnou hodnotu (i, j, k), proto budou v násle- dujícím textu tato čísla uvažována jako proměnné (i, j, k) a cykly budou referovány dle proměnné, kterou iterují. Třídy, implementující algoritmy ná- sobení, jsou pojmenovány tak, jak postupně proměnné i, j, k v nich přicházejí na řadu. Například třída MatrixMultiplicatorIKJ pracuje s proměnnou i ve vnějším cyklu, s proměnnou k v prostředním cyklu a konečně s proměnnou j v cyklu který je nejvíce vnořený. Pro tři do sebe vnořené cykly existuje šest různých kombinací jejich vzájemného pořadí.

IJK

Algoritmus třídy MatrixMultiplierIJK ve středním cyklu uloží do lokální pomocné proměnné prvek z matice A, určený souřadnicemi i a j. Při iterování vnitřního cyklu je prvek násoben s řádkem matice B a výsledek je ukladán do lokálního pomocného vektoru. Po skončení vnitřního cyklu je ve středním cyklu inkrementována proměnná j. Je změněna souřadnice prvku v matici A, a ten je aktualizován. Ve vniřním cyklu je opět vynásoben postupně s pří- slušným řádkem matice B (určeným souřadnicí j) a výsledek je přičítán do téhož pomocného vektoru. Teprve po skončení všech iterací středního cyklu je pomocný vektor vložen jako výsledný řádek do matice C. Hodnota i je ve vnějším cyklu inkrementována a ve dvou vnitřních cyklech je jiný sloupec matice A vynásoben postupně s celou maticí B.

(27)

Obrázek 6: Násobení v pořadí ijk.

IKJ

Z názvu algoritmu MatrixMultiplierIKJ, a z Obrázku 7 je patrné, že algo- ritmus nejčastěji přistupuje k prvkům matice B. Proměnné j, k určují právě její souřadnice a zároveň jsou v tomto algoritmu ve vnořených cyklech. Je vhodné v prvním cyklu načíst řádek číslo i v matici A. Druhý cyklus iniciali- zuje pomocnou proměnnou, do které bude ve třetím, vnitřním cyklu ukládán průběžný výsledek operace. Zamezí se tak častému přístupu do matice C.

Obrázek 7: Násobení v pořadí ikj.

Ve třetím cyklu je načtený řádek z matice A postupně násoben se sloupcem matice B a výsledek je přičítán do pomocné proměnné. Na konci iterace středního cyklu je pomocná proměnná vložena na souřadnice i, k do matice C. Následně je inkrementována proměnná k, která označuje pořadové číslo sloupce v matici B a upravuje souřadnici pro vložení prvku v matici C.

Je spuštěna další iterace středního cyklu. V té je opět opakovaně spouštěn vnitřní cyklus, kde je načtený řádek z A násoben s dalším sloupcem matice

(28)

B. Postupně je tak každý řádek z matice A násoben se všemi sloupci matice B a výsledky jsou po prvcích řádkově ukládány do matice C.

JIK

Ve vnějším cyklu metody MatrixMultiplierJIK bude načten řádek matice B, ve středním cyklu je pak do lokální proměnné uložen prvek z matice A, určený souřadnicemi i, j. Ve vnitřním cyklu bude prvek z A násoben po-

Obrázek 8: Násobení v pořadí jik.

stupně s celým připraveným řádkem z B a dílčí výsledky budou uloženy po řádcích do matice C. Během dvou vnořených cyklů bude matice C naplněna celá dílčími výsledky, zatímco z matice A bude přečten jeden sloupec a z ma- tice B jeden řádek. Při dalších iteracích vnějšího cyklu je důležité hodnoty v matici C nepřepisovat, další výsledky násobení prvků z A a řádku z B je třeba k již existujícím hodnotám v matici C přičíst.

JKI

V případě algoritmu MatrixMultiplierJKI je ve vnějším cyklu načten slou- pec matice A, určený souřadnicí j. Na začátku každé iterace středního cyklu je do pomocné proměnné načten prvek z matice B, určený souřadnicemi j, k.

Během vnitřního cyklu je prvek z matice B násoben s připraveným sloupcem z A a výsledky jsou ukládány, respektive přičítány, do sloupce matice C. Po skončení vnitřního cyklu a inkrementaci k v cyklu středním je nový prvek z matice B ve vnitřním cyklu opět násoben se stejným sloupcem matice A a výsledek je ukládán, respektive přičítán, do dalšího sloupce matice C, určeného proměnnou k.

(29)

Obrázek 9: Násobení v pořadí jki.

KIJ

Algoritmus MatrixMultiplierKIJ určí ve vnějším cyklu sloupec z matice B, se kterým bude pracovat, a načte jej do lokálního pomocného pole. Ve vnitřním cyklu je připravený sloupec z B násoben s řádkem matice A a vý- sledky násobení jsou přičítány do lokální pomocné proměnné. Po skončení

Obrázek 10: Násobení v pořadí kij.

vnitřího cyklu je hodnota pomocné proměnné vložena do matice C na souřad- nice určené i a k. Ve středním cyklu je inkrementováno i, které určuje pozici řádku v matici A a po vstupu do vnitřního cyklu je tento řádek opět vyná- soben se stále připraveným sloupcem matice B, výsledkem čehož je prvek určený k uložení do matice C.

KJI

Metoda algoritmu MatrixMultiplierKJI ve vnějším cyklu přiřadí hodnotu proměnné k a ihned vstoupí do středního cyklu. Zde je do pomocné proměnné

(30)

uložena hodnota prvku matice B ze souřadnic j, k. Ve vnitřním cyklu je

Obrázek 11: Násobení v pořadí kji.

připravený prvek matice B postupně násoben se sloupcem matice A, určeným hodnotou j, a výsledky jsou přičítány na příslušné pozice pomocného vektoru.

Jakmile střední cyklus projde všechny iterace proměnné j, je v pomocném vektoru kompletní výsledný sloupec matice C, který je po dokončení cyklu vložen do matice C na pozici k.

Testování a porovnání výsledků jednotlivých maticových násobiček je po- drobně rozpracováno v kapitole 4.

(31)

3 Implementace

Aplikace byla navržena dle schématu klient - server. Na tomto principu pracuje právě technologie RMI, která v aplikaci hraje klíčovou roli. Na stranu serveru patří implementace rozsáhlého programu pro práci s maticemi se schopností distribuovat pomocí technologie RMI po síti jak celé matice, tak jejich bloky. Jedná se především o balíčky server, matrix, reader, adder a multiplication. Klienstkou stranu aplikace představují především testovací třídy a různé utility, obsažené v balících tests, grid, client a tests.tools.

3.1 Server

Třída RMIRemoteFactory, je samostatný spustitelný program, který běží na všech uzlech, se kterými chceme pracovat. Jeho hlavní implementovanou metodou je getRemoteInstance(String className). Ta je schopná na po- žádání vracet instance vzdálených objektů, jež jsou specifikovány parame- trem className typu String. Při volání metody je parametrem předán přesný název třídy, a server zajistí vytvoření vzdálené instance a vrácení re- ference na ni (viz Tabulka 5). Protože metoda getRemoteInstance() vrací pouze objekty typu Remote, o jejich přetypování na správný typ se stará ten, kdo o vzdálenou instanci požádal.

Tabulka 5: Vrácení vzdálené instance objektu metodou getRemoteInstan- ce(String className)(pouze klíčový kód).

public Remote getRemoteInstance(String className) throws RemoteException { Class clazz = null;

clazz = Class.forName(className);

Constructor constructor = null;

constructor = clazz.getConstructor();

Remote remote = null;

remote = (Remote) constructor.newInstance();

return remote;

}

3.2 Klient

Klientská část aplikace hraje roli klasického klienta v systému RMI (viz Obrázek 2). Implementace klienta má za cíl otestovat veškerou funkčnost systému jak z hlediska korektnosti jednotlivých operací, tak z hlediska je- jich časové náročnosti. Klient obsahuje většinou spustitelné třídy, uložené

(32)

v mnoha balících podle testované oblasti. Třídy zpravidla generují výstup a výsledky, převážně údaje o délce trvání operace, je možné dále použít.

Klienstká část aplikace také obsahuje třídy, které umožňují připojení k serveru a organizaci práce se serverem či servery.

Připojení k serveru, tedy získání reference na běžící server, umožňuje třída ServerConnector z balíčku client. Je implementována dle návrhového vzoru Singleton. Její konstruktor je soukromý, tak je zajištěno že bude vytvo- řena pouze jedna instance jejíž referenci získáme voláním veřejné metody getServerConnector(). Třída ServerConnector implementuje veřejné roz- hraní IServerConnector, kde je deklarována metoda getServer(String nodeName, String serverName). Volání metody vrací referenci na RMIRe- motefactory typu IRemoteFactory.

Balíček grid obsahuje jedinou třídu, GridComputers (viz Obrázek 12).

Třída GridComputers je navržena a implementována podle návrhového vzoru Singleton. Její konstruktor je soukromý (private) a jediným způsobem, jak

Obrázek 12: Práce třídy GridComputers.

vytvořit instanci třídy a získat referenci na ni, je volání statické metody Grid- Computers.getInstance(). Ta zajistí, že za běhu aplikace bude vytvořena pouze jediná instance třídy. Po vytvoření instance dojde ke spojení s uzly v clusteru, a do statického listu hostNames jsou zapsány adresy uzlů, na nichž běží rmi server. Pro získání instance vzdáleného objektu musí klient vždy dodat volané metodě jméno třídy, jejíž instance má být vytvořena.

Vzdálené instance vrací výše popsaná metoda třídy RMIRemotefactory, ale je možné použít i metodu gridu getRemoteInstance(String className).

Metoda vrátí referenci na vzdálený objekt pokaždé z jiného serveru, prochází

(33)

stále dokola seznam uzlů od začátku dokonce. Snaží se tak rovnoměrně rozdělit zátěž. Pro získání reference na běžící server nemusí už klient znát jména uzlů v clusteru díky přetížené metodě getFactory(int h). Číselný argument metody specifikuje pořadí serveru v seznamu třídy GridCompu- ters. Pokud je hodnota čísla příliš velká, dokáže ji grid korigovat. Referenci na server lze stále získat udáním konkretního jména serveru, pomocí druhé verze metody getFactory(String hostName). V obou metodách je použito funkčnosti třídy ServerConnector

3.3 Matrix

Veškerá implementace týkající se uložení matic v paměti, čtení prvků, vytvá- ření matic a vkládání prvků je obsažena v balíku matrix. Jediné, co je potřeba pro práci s maticemi znát je rozhraní IMatrix. Po vytvoření vzdálené instance objektu matice je tento přetypován na IMatrix, a pro klienta jsou tak definovány metody, které rozhraní nabízí. Jedná se o bežné metody pro práci s maticemi, například čtení prvků, ukládání prvků na konkrétní po- zice, inicializace matice a nebo získávání informace o rozměrech matice. Po- tomky rohraní IMatrix jsou všechny třídy konkrétních maticových typů.

Pro práci s kompozitními maticemi je kvůli některým metodám, atypickým pro jednoduché matice, definováno rozhraní ICompositeMatrix, které im- plementuje pouze třída CompositeMatrix. IMatrix implementuje nejprve abstratkní třída AbstractMatrix, která zároveň rozšiřuje třídu Unicast- RemoteObject. Rozšíření třídy UnicastRemoteObject umožní pozdější ex- port jejích potomků, takže jsou přístupní přes RMI. Potomci AbstractMa- trix, Matrix a CompositeMatrix, konečně implementují metody deklaro- vané v rozhraní IMatrix, v případě CompositeMatrix i metody v rozhraní ICompositeMatrix.

Matice je reprezentována chráněnou proměnnou typu double[][]. Ini- cializace je možná předáním dvojrozměrného pole typu double[][]. Pře- dáním dvou parametrů inicializační metodě typu initMatrix(int, int) vznikne nulová matice o požadovaných rozměrech. Přístup k jednotlivým prvkům realizují metody get(int, int), put(int, int, double) (viz Ta- bulka 6 a Tabulka 7) a add(int, int, double). Přístup k řádkům matice implementují metody getRow(int), putRow(int, double[]), addRow(int, double[]) a přístup k sloupcům matice metody getCol(int), putCol(int, double[]) a addCol(int, double[]). Pro práci s celým polem prvků najed- nou byly implementovány metody getArray() pro získání existujícího pole z matice, putArray(double[][]) k vložení pole a metoda pro přičtení hod- not v novém poli k již existujícím v matici addArray(double[][]). Metody getNumRows() a getNumCols() umožňují zjistit rozměry matice.

(34)

Tabulka 6: Metoda umožňující vkládání prvků do jednoduché matice.

public void put(int i, int j, double value) throws RemoteException { if ( i > getNumRows() - 1 || j > getNumCols() - 1){

throw new RemoteException("Zapis mimo matici");

}

matrix[i][j] = value;

}

Kompozitní matice

Třída CompositeMatrix obsahuje namísto pole primitivních datových typů dvojrozměrné pole objektů typu IMatrix[][]. Díky vlastní implementanci metod pro přístup k prvkům je možno s kompozitní maticí pracovat stejně jako s jednoduchou maticí typu Matrix (viz Tabulka 7).

Tabulka 7: Metoda umožňující vkládání prvků do složené matice.

public void put(final int i, final int j, final double value) throws RemoteException { AccessCoor ai = createAccessCoor(i, j);

if (mat[ai.iMat][ai.jMat] == null) {

throw new RemoteException("Zapis mimo inicializovanou oblast");

}

mat[ai.iMat][ai.jMat].put(ai.iPos, ai.jPos, value);

}

Složená matice uchovává rozměry matic, které obsahuje, a díky souřad- nicovému systému dokáže určit konkrétní matici a konkrétní prvek v ní.

Práce s prvky kompozitní matice probíhá navenek stejně jako práce s prvky jednoduché matice. Inicializační metoda initMatrix(int m, int n), která v případě jednoduché matice vytvoří pole nulových prvků o zadaných rozmě- rech, zde vytvoří pouze pole referencí typu IMatrix. Hodnota těchto refe- rencí je null. Konkrétní matice se do pole vkládají pomocí metody insert- Matrix(int i, int j, IMatrix a). Metoda zajistí uložení matice na urče- nou pozici, zkontroluje, zda je tento nový blok kompatibilní s ostatními bloky, a upraví údaj o celkovém počtu řádků a sloupců. Metoda insert- Matrix(IMatrix a) také umožňuje vložit celou jednoduchou matici, která se při vkládání rozdělí na několik menších. Kritéria rozdělení lze nastavit metodou initMatrix(int m, int n) (viz Tabulka 8). Třída také dokáže vrátit odkaz na konkrétní matici obsaženou v poli IMatrix[][] pomocí metody getIMatrix(int i, int j).

(35)

Tabulka 8: Inicializační metoda pro CompositeMatrix.

public void initMatrix(int m, int n) throws RemoteException { rowLength = new int[m];

for (int i = 0; i < rowLength.length; ++i) { rowLength[i] = -1;

}

colLength = new int[n];

for (int j = 0; j < colLength.length; ++j) { colLength[j] = -1;

}

mat = new IMatrix[m][n];

for (int i = 1; i < rowLength.length; ++i) { rowLength[i] += rowLength[i - 1];

}

for (int j = 1; j < colLength.length; ++j) { colLength[j] += colLength[j - 1];

} }

3.4 Maticové operace

Všechny komponenty aplikace, zabývající se maticovými operacemi, mají vlastní centrální třídu, takzvaný manažer. Manažer neimplementuje žádný konkrétní algoritmus načítání nebo výpočtu, ale stará se o veškerou režii spojenou s danou operací. Přijímá vstupní argumenty klienta, v případě ope- rací s kompozitními maticemi plánuje rozdělení zátěže na clusteru, vytváří vlákna s konkrétními operacemi a zajišťuje předání výsledků klientovi. Kód manažerů bude podrobněji popsán v příslušné kapitole každé komponenty.

Manažer na konci své práce vrátí klientovi referenci typu IThreaded- Operation. Jedná se o rozhraní, které deklaruje metodu join(). Rozhraní IThreadedOperation rozšiřují všechna rozhraní v jednotlivých komponen- tách, která deklarují metody tříd konkrétních algoritmů. V balíčku každé komponenty je metoda join() příslušným způsobem implementována. Je to z důvodu zjednodušení používání komponent maticových operací klientem.

Ten pro každou operaci získá referenci na instanci příslušného manažera, voláním icializační metody předá argumenty a zpět dostane objekt typu IThreadedOperation. Objekt obsahuje běžící vlákna, která provádějí nebo dokončují maticovou operaci. Pomocí metody join() počká klient na doběh vláken a v určené matici, jejíž referenci předal manažerovi, najde výsledek operace. Rozhraní IThreadedOperation je umístěno v odděleném balíku operation.

V balíku operation je ještě umístěna abstraktní třída AbstractMatrix- Operation, která implementuje kód společný pro všechny maticové operace.

Při volání inicializační metody klientem každý manažer převezme, mimo jiné, reference na maticové objekty, se kterými má být požadovaná operace prove-

(36)

dena. Veškerý kód algoritmů maticových operací je možné spouštět ve vláknu, protože je celý umístěn do přepsané metody run(), která však nepřijímá žadné argumenty. Aby tedy měly konkrétní algoritmy přístup k maticím, se kterými mají pracovat, je třeba jim reference na ně nějak doručit. K tomu slouží třída AbstractMatrixOperation. Reference na maticové objekty uloží do svých soukromých proměnných, a pomocí metod tyto zprostřekduje svým potomkům, kteří se zabývají výpočty. Třídu AbstractMatrixOperation roz- šiřuje v každém balíku maticových operací vlastní abstraktní třída, která do- plní ještě implementaci metody testOperation(). Metoda testOperation() provádí test správného zadání matic pro danou operaci, je deklarována ve třídě AbstractMatrixOperation. Potomky zmíněné abstraktní třídy každé komponenty jsou třídy implementující přímo algoritmy pro načítání, sčítání a násobení matic, takzvané konkrétní čtečky, sčítačky a násobičky.

Načítání matic ze souboru

Komponenta pro načítání matic ze souboru je v balíku reader a centrální třídou je tu MatrixReaderManager. Funkce čtečky je poměrně jednoduchá, z daného souboru přečíst všechny prvky, a vložit jejich hodnoty na správná místa v maticovém objektu. Cestu k souboru a referenci na matici manažer obdrží jako argumenty od klienta při volání metody initReading(String, IMatrix). V metodě je zjišten typ matice, zda je kompozitní, nebo jednodu- chá, a v závislosti na tom se provedou další kroky. V případě jednoduché matice je pomocí metody getHostName() zjištěn její hostitelský stroj, na kterém je vytvořena čtečka. Následně je čtečka inicializována, spuštena a ob- jekt s běžícím vláknem je vrácen klientovi (viz Tabulka 9).

Tabulka 9: Vytvoření, inicializace a spuštění čtečky pro jednoduchou matici.

Kód metody initReading(String, IMatrix)

IRemoteFactory factory = grid.getFactory(hostName);

IMatrixReader singleReader = (IMatrixReader) factory .getRemoteInstance("reader.MatrixReader");

singleReader.initialize(matrix, fileName);

singleReader.start();

return (IThreadedOperation) singleReader;

Je-li manažerovi předána kompozitní matice, je třeba zjistit zda byla ini- cializována, a zda obsahuje nějaké konkrétní matice jako své bloky. Pokud ob- sahuje pouze reference na nulové objekty typu IMatrix, nelze načítat. Na zá- kladě počtu bloků kompozitní matice a rozměrů matice v souboru jsou nulové bloky inicializovány správnými rozměry, aby bylo možné do matic načítat konkrétní prvky na konkrétní souřadnice. V dalším kroku je deklarováno

(37)

pole čteček readerArray typu IMatrixReader. Pro každý blok kompozitní matice bude na jeho hostitelském stroji vytvořena vlastní čtečka. Poté, co jsou všechny čtečky vytvořeny a spuštěny vznikne instance speciálního ob- jektu příkazem IMatrixReader reader = new CompositeReader(reader- Array). Konstruktoru třídy CompositeReader je pole čteček předáno jako argument. Třída pak přepisuje metodu join() tak, aby bylo možné kon- trolovat doběh všech vláken uložených v poli jediným voláním. Objekt třídy CompositeReader je nakonec přetypován na typ IThreadedOperation a vrá- cen stejně jako v případě práce s jednoduchou maticí.

Sčítání matic

Třídy komponenty pro sčítání matic jsou uloženy v balíku adder. Ústřední třídou komponenty je MatrixAdderManager, manažer maticového sčítání.

Klient spustí sčítání matic voláním metody manažera initAdding(IMatrix, IMatrix, IMatrix). Jako argumenty předá manažerovi reference na tři ma- tice. Sčítací manažer nyní ověří, je-li možné matice sčítat (mají-li vhodné rozměry) a zjistí typy matic (kompozitní, jednoduché) pomocí metody getTy- pe(). V případě sčítání jednoduchých matic manažer dále zjistí hostitelský stroj každé z nich, zvolí jeden a pomocí třídy GridComputers na něm získá referenci na RMIRemoteFactory. Pomocí této továrny vytvoří vzdálenou in- stanci sčítačky typu IMatrixAdder a její referenci uloží do lokální proměnné (viz Tabulka 10). Typ sčítačky, určený hodnotou v proměnné adderType, je zvolen v závislosti na testovacím scénáři. Operaci není nutné spouštet, činí tak automaticky metoda initialize(IMatrix, IMatrix, IMatrix) abstraktní třídy AbstractMatrixAdder. Nakonec je proměnná s referencí na sčítačku přetypována na IThreadedOperation a vrácena jako návratová hodnota metody initAdding(IMatrix, IMatrix, IMatrix).

Tabulka 10: Vytvoření, inicializace a spuštění sčítačky jednoduchých matic.

Kód metody initAdding(IMatrix, IMatrix, IMatrix)

IRemoteFactory factory = grid.getFactory(host);

IMatrixAdder adderSimple = (IMatrixAdder) factory

.getRemoteInstance(addersListClassNames.get(adderType));

adderSimple.initialize(matrixA, matrixB, matrixC);

return (IThreadedOperation) adderSimple;

Pro případ kompozitních matic je postup podobný. Každé dva určité bloky z matic určených k sečtení dají jeden kompletní blok výsledné matice.

Algoritmus, který provádí sčítání jednoduchých matic, je opakován v cyklu pro každou trojici bloků tří zadaných matic. Kód zobrazený v Tabulce 11 vy-

References

Related documents

Naznačte způsob vykazování čistých hodnot výnosů/nákladů ve Výkazu zisku a ztráty pojišťoven (zajišťoven) na příkladu těchto položek: a) zasloužené pojistné

Dle výše popsaným výpočtům lze následně provést rozdělení obecné plochy na potřebné jednotlivé ele- menty tak, aby vlastní obrábění těchto částí probíhalo

V práci je proto nejprve provedena diskuse a návrh původních algoritmů fuzzy transformace pro aproximaci obrazové funkce, kterých je potom následně využito

Fuzzy zpracování obrazu má tři hlavní fáze: kódování obrazových dat (fuzzifikace obrazu), modifikace hodnot příslušnosti do fuzzy mnoţiny (systém fuzzy rozpoznávání

V této kapitole se budeme věnovat praktickým aplikacím a prezentaci algoritmů s využitím fuzzy logiky při zpracování obrazu v prostředí LabVIEW, které jsme teoreticky popsali

V rámci komplexního hodnocení žmolkovitosti textilií je v této práci brán ohled nejen na kvantitativní charakteristiky žmolků, které slouží pro popis žmolkovitosti

Kvantitativní analýzou je myšleno určení množství nebo koncentrace složek v měřeném vzorku, které charakterizuje plocha píku. V dnešní době je plocha píku

[r]