Software Engineering für moderne, parallele Plattformen 5. Parallelität in Java Dr. Victor Pankratius Dr. Victor Pankratius IPD Tichy- Lehrstuhl für Programmiersysteme KIT – die Kooperation von Forschungszentrum Karlsruhe GmbH und Universität Karlsruhe (TH) Agenda Erzeugen von Fäden Konstrukte zum Schützen kritischer Abschnitte Konstrukte für Warten und Benachrichtigung Java.util.concurrent Kollektionen Asynchrone Ausführung / Thread Pools Synchronisierer 2 Dr. Victor Pankratius Konstrukte zum Erzeugen von Parallelität Seit Java 1.0: Eingebaute Klassen und Schnittstellen für parallele Programmierung: Interface java.lang.Runnable public interface Runnable { public abstract void run(); } Klasse java.lang.Thread public class public public public public … } 3 Dr. Victor Pankratius Thread implements Runnable { Thread(String name); Thread(Runnable target) void start(); void run(); Konstrukte zum Erzeugen von Parallelität Faden erzeugen Methode 1 Methode 2 Implementieren der Schnittstelle Runnable Anlegen einer Subklasse von Thread Implementieren der Methode Run() Überschreiben der Methode run() Übergabe einer Instanz an einen Konstruktor eines Thread-Objekts thread Anlegen einer Instanz thread der Subklasse Aufruf der Methode thread.start() 4 Dr. Victor Pankratius Konstrukte zum Erzeugen von Parallelität Beispiel: Methode 1 Klasse, die Runnable implementiert: class ComputeRun implements Runnable { long min, max; ComputeRun(long min, long max) { this.min = min; this.max = max; } public void run() { // Parallele Aufgabe } } Erzeuge und starte Kontrollfaden: ComputeRun c = new ComputeRun(1,20); new Thread(c).start(); 5 Dr. Victor Pankratius • Starten des neuen Kontrollfadens. Erst das erzeugt die neue Aktivität • Die Methode start() kehrt sofort zurück, der neue Kontrollfaden arbeitet nebenläufig weiter. • Kein Neustart: start() darf nur einmal aufgerufen werden. • run() nicht direkt aufrufen Konstrukte zum Erzeugen von Parallelität Beispiel: Methode 2 Klasse, die von Thread erbt: class ComputeThread extends Thread { long min, max; ComputeThread(long min, long max) { this.min = min; this.max = max; } public void run() { // Parallele Aufgabe } } Erzeuge und starte Kontrollfaden: ComputeThread t = new ComputeThread(1,10); t.start(); 6 Dr. Victor Pankratius Lebenszyklus eines Fadens Ein Faden kann in einem von sechs Zuständen sein (abfragen mit getstate()) NEW Faden wurde erstellt, aber start() noch nicht aufgerufen RUNNABLE Faden wird ausgeführt BLOCKED Faden wird nicht ausgeführt, weil er auf Ressource wartet (z.B. Sperre) WAITING Faden wird nicht ausgeführt, weil er Object.wait() oder Thread.join() aufgerufen hat TIMED_WAITING Faden wird nicht ausgeführt, weil er Thread.sleep() bzw. Object.wait() oder Thread.Join() mit einem Timeout-Wert aufgerufen hat TERMINATED Ausführung ist beendet. Methode run() wurde normal beendet oder durch auslösen einer Ausnahme 7 Dr. Victor Pankratius Kritische Abschnitte Monitor-Konzept: Jedes Objekt kann Monitor sein Jedes Objekt hat implizit assoziierte Sperre Gesperrt wird automatisch mit Hilfe des synchronized-Konstrukts Synchronized ist block-orientiert /*synchronisierte Methode*/ synchronized void foo(){ // Rumpf ist kritischer // Abschnitt } 8 Dr. Victor Pankratius Alternative: ... /*synchronisierter block*/ synchronized (obj) { // kritischer Abschnitt } ... Kritische Abschnitte Sperrkonstrukte Ab Java 5: Zusätzliches Sperrkonstrukt ohne Blockorientierung, das explizit gesperrt/entsperrt werden kann: Lock in java.concurrent.locks //neu in Java 5 import java.util.concurrent.locks.*; Lock lock = new ReentrantLock(); //funktioniert //Rekursion … lock.lock(); … lock.unlock(); 9 Dr. Victor Pankratius auch bei Kritische Abschnitte Sperrkonstrukte Beispiel für weiteres Sperrkonstrukt in java.util.concurrent.locks ReentrantReadWriteLock Nützlich für parallele Algorithmen, die Datenstruktur häufig lesen und selten schreiben bzw. aktualisieren Besitzt ein Paar von getrennten Lock-Objekten für Lese- und Schreibsperren (readLock, writeLock) Eine beliebige Anzahl von Fäden kann mit readLock() die Lesesperre acquirieren, solange kein Faden die Schreibsperre acquiriert oder bereits gesetzt hat Wenn Faden Schreibsperre setzen will, werden keine neuen Lesesperren mehr elaubt. Nachdem alle Leser ihre Lesesperren freigegeben haben, acquiriert der Schreiber die Schreibsperre. Es werden keine Lesezugriffe mehr erlaubt, bis Schreiber fertig ist. Ein Schreiber kann Schreibsperre zu einer Lesesperre “umwandeln”, indem er eine Lesesperre acquiriert und dann die Schreibsperre freigibt 10 Dr. Victor Pankratius Sperrkonstrukte Performanzvergleich Doug Lea: The java.util.concurrent synchronizer framework Science of Computer Programming, Elsevier North-Holland, Inc., 2005, 58, 293-309 Auszüge Benchmark-Programm Ein Faden ruft mit Wahrscheinlichkeit S gemeinsam genutzten Zufallszahlengenerator auf, der durch Sperre geschützt wird. Mit Wahrscheinlichkeit 1-S wird lokaler Zufallszahlengenerator benutzt. Zweck der Randomisierung Zeitpunkt, wann Sperre gesetzt wird Code in Schleifen kann nicht trivialerweise optimiert werden Tests auf 4 x86-Maschinen (Linux 2.4) und 4 UltraSparc-Maschinen (Solaris 9), SUN J2SE 5.0 JDK; alle Programme wurden vor den Messungen 20x ausgeführt 11 Dr. Victor Pankratius Sperrkonstrukte Performanzvergleich Auszüge (fortgesetzt) Experimente mit verschiedenen Typen von Sperren Builtin: Blockbasiertes Synchronized-Konstrukt Mutex: Eigene, einfache Mutex-Klasse, die eine explizite Sperre implementiert Reentr: ReentrantLock Faden, der die Sperre hält, kann Sperrmethoden wieder aufrufen. Diese kehren ohne Synchronisierungsaufwand sofort zurück. getHoldCount() gibt an, wie oft der Faden gesperrt hat; beim Freigeben muss unlock() muss ebenso oft aufgerufen werden fair: ReentrantLock im „fair“-Modus Bevorzugt länger wartende Fäden beim Zuteilen der Sperre 12 Dr. Victor Pankratius Sperrkonstrukte Performanzvergleich Builtin: synchronized-Konstrukt Mutex: explizite sperre Reentr: ReentrantLock fair: ReentrantLock im „fair“-Modus Schätzungen der Overhead-Zeiten in ns Name builtin mutex reentr fair builtin mutex reentr fair 1P 2P 2A 4P 1U 4U 8U 24U 18 58 13 116 90 122 160 161 9 71 21 95 40 82 83 84 31 77 31 109 58 100 103 108 37 81 30 117 67 115 123 119 521 930 748 1146 879 2590 1274 1983 46 108 79 188 153 347 157 160 67 132 84 247 177 368 174 182 8327 14967 33910 15328 41394 30004 31084 32291 Overhead der Sync.-Konstrukte bei Ausführung mit 1 Faden Zeit ist Differenz zwischen Code mit Synchronisierung (S=1) vs. Code ohne Synchronisierung (S=0) 13 Dr. Victor Pankratius Testrechner 1P (1 × 900 MHz Pentium 3) 2P (2 × 1400 MHz Pentium 3) 2A (2 × 2000 MHz Athlon) 4P (2 × 2400 MHz hyperthreaded Xeon) 1U (1 × 650 MHz Ultrasparc2) 4U (4 × 450 MHz Ultrasparc2) 8U (8 × 750 MHz Ultrasparc3) 24U (24 × 750 MHz Ultrasparc3) Mehraufwand (Overhead) pro Sperre bei S = 1 (jeder Faden ruft immer gemeinsamen Generator auf) und 256 parallelen Fäden Klassen mit atomaren Operationen java.util.concurrent.atomic Enthält Klassen mit atomaren Operationen auf Datentypen wie Boolean, Integer, Referenztypen Beispiel: AtomicInteger Atomar ausführbare Operationen compareAndSet(int expect, int update) addAndGet(int delta) getAndAdd(int delta) decrementAndGet() getAndIncrement() … 14 Dr. Victor Pankratius Koordination: Konstrukte für Warten und Benachrichtigung (1) Manchmal müssen Fäden ihre Ausführung stoppen (und Sperren freigeben), bis ein bestimmtes Ereignis eintritt und erst danach ihre Ausführung fortsetzen Methoden in java.lang.Object public final void wait() throws InterruptedException; public final void notify(); public final void notifyAll(); 15 Dr. Victor Pankratius Koordination: Konstrukte für Warten und Benachrichtigung (2) Jedes Objekt verwaltet eine interne Warteschlange mit wartenden Fäden Wenn ein Faden die wait-Methode eines Objekts o aufruft, dann wird die mit o assoziierte Sperre, die der Faden hält, temporär freigegeben anschließend wird der Faden in die Warteschlange des Objekts o eingereiht und schlafen gelegt Wenn ein anderer Faden notifyAll auf demselben Objekt o aufruft, dann weckt o alle Fäden in der Warteschlange auf und ermöglicht ihnen die weitere Ausführung notify() schickt Signal an irgendeine Aktivität aus dieser Warteschlange notifyAll() schickt Signal an alle Aktivitäten dieser Warteschlange 16 Dr. Victor Pankratius Beispiel zu Wait / NotifyAll import java.util.*; /** * Eine Warteschlange. Ein Faden ruft put() auf um ein Objekt in die * Schlange zu legen. Ein anderer Faden ruft get() auf um ein Objekt aus * der Schlange zu nehmen. Wenn Schlange leer ist, wartet get(), bis ein * Objekt da ist (d.h. bis Nachricht von notify da). */ public class WaitingQueue<E> { LinkedList<E> q = new LinkedList<E>(); // Speichert Objekte public synchronized void q.add(o); this.notifyAll(); } put(E o) { // Füge Objekt am Ende der Liste hinzu // Benachrichtige wartende Fäden, // dass Objekt da public synchronized E get() { while(q.size() == 0) { try {this.wait(); } catch (InterruptedException ignore) {} } return q.remove(0);} } 17 Dr. Victor Pankratius Ergänzungen in der Java-Bibliothek java.util.concurrent Die Verwendung von expliziten bzw. feingranularen Sperren ist oft fehleranfällig Viele der Datenstrukturen (z.B. Schlangen) aus dem sequenztiellen Fall sind im parallelen Fall nicht verwendbar, wenn mehrere Fäden simultan darauf arbeiten (engl. „not thread-safe“) Ab Java 1.5: java.util.concurrent stellt weitere Klassen zur parallelen Programmierung zur Verfügung Konzepte / Terminologie ähneln z.T. Ada (vgl. Burns & Wellings, Concurrent and Real-Time Programming in Ada, Cambridge, 2007) 18 Dr. Victor Pankratius java.util.concurrent Bereitstellung nützlicher Funktionalität, die „immer wieder“ gebraucht wird Kategorien Kollektionen („Collections“) Konstrukte zur asynchronen Ausführung, Thread Pools Synchronisierer („Synchronizers“) 19 Dr. Victor Pankratius java.util.concurrent Concurrent Collections Eine Kollektion ist eine Gruppe von Objekten Für sequenzielle Verarbeitung bietet Java bereits im “Java Collections Framework” (java.util) entsprechende Datenstrukturen, z.B. Set: Kollektion ohne Duplikate (Operationen: add, remove, contains,...) SortedSet: Set, in dem Elemente sortiert werden Verschiedene Set-Implementierungen basierend auf Hash-Tabellen, Rot-Schwarz-Bäumen, Arrays, Bitfeldern List: Kollektion, in der Elemente eine Ordnung haben; Duplikate sind erlaubt Map: Menge von Schlüssel/Wert-Paaren (Operationen: put(key, value), get(key), containsKey, …) Operationen auf diesen Datenstrukturen sind nicht atomar 20 Dr. Victor Pankratius java.util.concurrent Concurrent Collections Ab Java5: java.util.concurrent bietet zusätzliche Kollektionsdatenstrukturen sowie atomare Operationen für sichere Nutzung im parallelen Fall, z.B.: ConcurrentHashMap CopyOnWriteArrayList CopyOnWriteArraySet ConcurrentLinkedQueue Einige benutzen aus Performanzgründen keine synchronizedKonstrukte, sondern explizite Sperren und volatile-Modifizierer (volatile bewirkt, dass ein Ergebnis nicht im Zwischenspeicher belassen wird, sondern immer aktualisiert wird. Die parallelen ausgeführten Fäden sehen somit immer den korrekten Variablenwert, da er vor jeder Benutzung aus dem Speicher gelesen und nach einer Änderung sofort wieder zurückgeschrieben wird. Für details (insb. Zusammenhang zu „happens-before“-Relation) vgl. Beschreibung des JavaSpeichermodells. 21 Dr. Victor Pankratius java.util.concurrent Concurrent Collections Beispiele ConcurrentHashMap Thread-sichere Implementierung der java.util.Map Schnittstelle Benutzt kein synchronized Beliebig viele parallele Leseoperationen ohne Sperren erlaubt Für Schreibzugriffe wird Datenstruktur intern in Segmente unterteilt, die beim Aktualisieren für die Lese-und Schreibzugriffe anderer Fäden gesperrt werden Größe der Segmente kann angepasst werden CopyOnWriteArrayList Thread-sichere Implementierung der java.util.List Schnittstelle Benutzt zur Implementierung ein Array Alle Update-Methoden benutzen synchronized 22 Dr. Victor Pankratius java.util.concurrent Modell zur asynchronen Ausführung Situation vor Java 1.5: Rückgabe eines Wertes von einem neu gestarteten Faden an den aufrufenden Faden war umständlich Im Prinzip über gemeinsam genutzte Variable und Synchronisation 23 Dr. Victor Pankratius java.util.concurrent Modell zur asynchronen Ausführung Neuer Ansatz ab Java 1.5 Der aufgerufene Faden implementiert die callable Schnittstelle (d.h. es wird die call-Methode anstatt run implementiert) Objekt, das callable implementiert, repräsentiert eine Aufgabe, die ein Ergebnis liefert und eine Ausnahme auslösen kann Der aufrufende Faden übergibt ein Objekt, das callable implementiert, mittels submit() an einen Executor und setzt danach seine Ausführung fort Submit liefert ein Future-Objekt zurück Aufrufer liest Ergebnis mittels get() aus Future-Objekt public interface Callable<V> { public V call() throws Exception; } 24 Dr. Victor Pankratius java.util.concurrent Modell zur asynchronen Ausführung Future Repräsentiert Ergebnis einer Berechnung, das sofort oder in der Zukunft verfügbar sein wird Funktionalität wie bereits besprochen get() liefert das Ergebnis der Berechnung oder blockiert so lange, bis das Ergebnis vorhanden ist Typische Nutzung: Future ist Ergebnis der asynchronen Ausführung einer Callable<V> Aufgabe in einem ExecutorService public interface Future<V> { public V get(); public V get(long timeout, TimeUnit unit); public boolean cancel(boolean mayInterruptIfRunning); public boolean isCancelled(); public boolean isDone() ; } 25 Dr. Victor Pankratius java.util.concurrent Beispiel: Future class CallableExample implements Callable<String> { public String call() { String result = “Arbeit fertig”; // tue was return result; } } ExecutorService es = Executors.newSingleThreadExecutor(); Future<String> f = es.submit(new CallableExample()); //tue was in der Zwischenzeit try { String callableResult = f.get(); } catch (InterruptedException ie) {...} catch (ExecutionException ee) {...} 26 Dr. Victor Pankratius java.util.concurrent Executors (1) Executors führen Aufgaben mit Hilfe von Fäden aus Aufgaben werden in einer internen Schlange abgelegt Executor ist eine Schnittstelle; enthält die Methode execute ExecutorService erweitert Executor um Verwaltungsmethoden wie submit, shutdown, invokeAll, … Executors ist eine Fabrik, die verschiedene Implementierungen von ExecutorService liefert 27 Single Thread Executor: Hat genau einen Arbeiterfaden Fixed Thread Pool: Thread Pool mit fester Anzahl von Fäden Cached Thread Pool: Thread Pool, der Anzahl von Fäden je nach Bedarf erzeugt Scheduled Thread Pool: Erlaubt periodische Ausführung von Aufgaben oder mit bestimmter Verzögerung Dr. Victor Pankratius java.util.concurrent Executors (2) - Beispiel class WebServer { Executor pool = Executors.newFixedThreadPool(7); public static void main(String[] args) { ServerSocket socket = new ServerSocket(80); while (true) { final Socket connection = socket.accept(); Runnable r = new Runnable() { public void run() { handleRequest(connection); } }; pool.execute(r); } } } 28 Dr. Victor Pankratius java.util.concurrent Schlangen Weitere Ergänzungen Produzent Blockierende Schlangen Implementieren BlockingQueue-Schnittstelle Datenaustausch zwischen Fäden gemäß dem Erzeuger-VerbraucherMuster Blockierende Blockierende Methoden: put, take Erzeuger blockiert, wenn Schlange voll; Verbraucher blockiert, wenn Schlange leer Schlange Auch zeitlich beschränke Blockierung möglich offer/poll blockieren nur für eine bestimmte Zeit Konsument 29 Dr. Victor Pankratius java.util.concurrent Schlangen Verschiedene Implementierungen ArrayBlockingQueue basiert auf Array mit fester Größe DelayQueue ordnet Elemente nach Wartezeiten an LinkedBlockingQueue basiert auf verketteter linearer Liste PriorityBlockingQueue Schlange mit unbegrenzter Kapazität, die Elemente gemäß einer Vergleichsoperation anordnet SynchronousQueue basiert auf Schlange mit Kapazität 0. Gedacht als „Übergabe“ bzw. Rendezvous zwischen Fäden (put wartet bis take aufgerufen wird oder umgekehrt). 30 Dr. Victor Pankratius java.util.concurrent Beispiel: ArrayBlockingQueue Beispiel Hauptfaden hat eine Schlange, die 10 Zahlen umfassen kann befüllt die Schlange mit Zahlen erzeugt mehrere Arbeiterfäden Arbeiterfaden nimmt eine Zahl aus der Schlange und quadriert sie Blockiert, wenn Schlange leer Bricht ab, wenn spezielle Markierung gelesen wird 31 Dr. Victor Pankratius java.util.concurrent Beispiel: ArrayBlockingQueue Arbeiter class Worker Hauptfaden extends Thread { //Marker zum Beenden der Arbeit static final Integer NO_MORE_WORK = new Integer(0); BlockingQueue<Integer> q; Worker(BlockingQueue<Integer> q) { this.q = q; } public void run() { try {while (true) { //nimm nächste Zahl oder //blockiere, wenn leer Integer x = q.take(); //Abbruch, wenn Marker gefunden if (x == NO_MORE_WORK) {break;} // Arbeit int y = x * x;} } catch (InterruptedException e) {} } 32 } Dr. Victor Pankratius // Erzeuge ArrayBlockingQueue mit Integer final int capacity = 10; BlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(capacity); // Erzeuge Arbeiterfäden final int numWorkers = 2; Worker[] workers = new Worker[numWorkers]; for (int i=0; i<workers.length; i++) { workers[i] = new Worker(queue); workers[i].start(); } try { //Füge Arbeit in Queue hinzu; //blockiere, wenn voll for (int i=1; i<100; i++) { queue.put(i); } //Füge Marker ein, die Worker Ende //signalisieren for (int i=0; i<workers.length; i++) { queue.put(Worker.NO_MORE_WORK); } } catch (InterruptedException e) {} java.util.concurrent Synchronisierer Werden zur Synchronisation zwischen Fäden verwendet Semaphore Wird mit einer fixen Anzahl von “Genehmigungen” initialisiert; acquire blockiert, bis eine Genehmigung verfügbar ist und dekrementiert anschließend die Anzahl der Genehmigungen Release inkrementiert die Anzahl der Genehmigungen 33 Dr. Victor Pankratius java.util.concurrent Synchronisierer Exchanger Ermöglicht Rendezvous und Datenaustausch zwischen zwei Fäden mittels der exchange-Methode Jeder Faden ruft exchange mit einem Objekt auf (das ausgetauscht werden soll) und bekommt nach dem Rendezvous das vom anderen Faden übergebene Objekt Der zuerst ankommende Faden blockiert so lange, bis der zweite exchange aufruft 34 Dr. Victor Pankratius java.util.concurrent Synchronisierer - Beispiel: Exchanger class FillAndEmpty { Exchanger<DataBuffer> exchanger = new Exchanger(); DataBuffer initialEmptyBuffer = ... DataBuffer initialFullBuffer = ... class FillingLoop implements Runnable { public void run() { DataBuffer currentBuffer = initialEmptyBuffer; try { while (currentBuffer != null) { addToBuffer(currentBuffer); if (currentBuffer.full()) currentBuffer = Jeder Faden übergibt ein Objekt an exchangeMethode und erhält das vom jeweils anderen Faden übergebene Objekt. exchanger.exchange(currentBuffer); } Idee: } catch (InterruptedException ex) {...handle...}} } class EmptyingLoop implements Runnable { public void run() { DataBuffer currentBuffer = initialFullBuffer; try { while (currentBuffer != null) { takeFromBuffer(currentBuffer); if (currentBuffer.empty()) currentBuffer = exchanger.exchange(currentBuffer); } } catch (InterruptedException ex) {...handle...} } 35 } } void start() { new Thread(new FillingLoop()).start(); new Thread(new EmptyingLoop()).start(); } Dr. Victor Pankratius •Puffer können zwischen Fäden ausgetauscht werden •Faden, der Puffer füllt, übergibt vollen Puffer an Faden, der Puffer leert •Faden, der Puffer leert, übergibt leeren Puffer an Faden, der Puffer füllt java.util.concurrent Synchronisierer CyclicBarrier Synchronisiert Gruppe von n Fäden. Fäden rufen await()-Methode auf, die so lange blockiert, bis alle n Fäden warten. Danach wird den Fäden erlaubt, ihre Ausführung fortzusetzen (die Barriere wird zurückgesetzt). Erweiterung: Zusätzliche Runnable-Methode wird ausgeführt, wenn der letzte von n Fäden await() aufgerufen hat. 36 Dr. Victor Pankratius java.util.concurrent Synchronisierer CountDownLatch Synchronisiert mehrere Fäden Jeder Faden ruft await() auf und blockiert, bis countDown() count mal aufgerufen wurde Danach wird allen Fäden erlaubt, ihre Ausführung fortzusetzen Erneuter Aufruf von await() hat anschließend keinen Effekt mehr 37 Dr. Victor Pankratius java.util.concurrent Beispiel: CountDownLatch Personen warten vor Eingangstür eines Einkaufszentrums mit mehreren Geschäften Zunächst sind alle Geschäfte noch geschlossen, werden aber sukzessive geöffnet (CountDown sukzessive aktualisiert) Erst wenn alle Geschäfte geöffnet haben, wird die Eingangstür zum Einkaufszentrum geöffnet. Dann können die wartenden Personen ins Einkaufszentrum gehen und ihre Einkäufe erledigen wartende Fäden (hier: Personen) CountDownLatch Aktiver Countdown-Faden (hier: herunterzählen der noch geschlossenen Geschäfte) 38 Dr. Victor Pankratius CountDownLatch wartende Fäden werden aktiviert, sobald der Countdown 0 erreicht hat (hier: Personen kaufen ein, sobald alle Geschäfte geöffnet haben) java.util.concurrent Synchronisierer - Beispiel: CountdownLatch public class CountDownLatchSample { private CountDownLatch latch; private int shops = 5; private class Shopper implements Runnable{ public void run() { try {latch.await();} catch (InterruptedException e) {...} System.out.println("Juhu, shopping!");} } private class Shop implements Runnable { public void run() { System.out.println("Opening shop.."); latch.countDown(); public static void main(...) { new CountDownLatchSample().simulate(); } } } } public CountDownLatchSample() { latch = new CountDownLatch(shops); 39} private void simulate() { int shopper = 10; for (int i = 0; i < shopper; i++) { Thread t=new Thread(new Shopper()); t.start(); } for (int i = 0; i < shops; i++) { Thread t = new Thread(new Shop()); t.start(); } } Dr. Victor Pankratius