Closures II (Java)

In Java gibt es schon seit recht frühen Versionen die sogenannten inneren Klassen.

Davon gibt es einige Ausprägungen:

  • statische innere Klassen
  • nicht-statische innere Klassen
  • anonyme innere Klassen

Beispiel 1: statische innere Klasse:

public class MyOuterClass {
  private static class MyInnerClass {
    ...
  }
}

Beispiel 2: nicht-statische innere Klasse:

public class MyOuterClass {
  private class MyInnerClass {
    ...
  }
}

Beispiel 3: anonyme innere Klasse:

public class MyOuterClass {
  public Runnable myMethod(..) {
    return new Runnable() {
      public void run() {
        ...
      }
   }
}

Sind die statischen inneren Klassen nichts anderes als weitere Klassen, die halt nur in ihrer Sichtbarkeit speziell für die umgebende Klasse zugänglich sind, so sind die anderen beiden Ausprägungen viel weitgehender in die umgebende Klasse integriert. Alle Attribute und Methoden der umgebenden Klasse können auch von der inneren Klasse aus angesprochen werden. Es fehlt leider ein zum „super“ analoges Schlüsselwort „outer“, so dass man sich mit Konstrukten wie „MyOuterClass.this.method()“ behelfen muss, wenn man etwas ansprechen will, was sowohl in der inneren als auch in der umgebenden Klasse vorkommt.

Genauer betrachtet gilt das für die statischen inneren Klassen und die statischen Attribute und Methoden der umgebenden Klasse natürlich auch, wobei man hier einfach „MyOuterClass.myStaticMethod()“ aufrufen kann.

Um in den nicht-statischen und anonymen inneren Klasse auf die umgebenden Attribute und Methoden zugreifen zu können, muss also implizit eine Instanz der äußeren Klasse referenziert werden. Das sollte man beachten, weil diese Referenz natürlich gehalten wird und so eine innere Klasse verhindert, dass die umgebende Instanz der Garbagecollection zugeführt wird. Beim Serialisieren kann ein scheinbar kleines Objekt riesig werden, wenn es einer solchen inneren Klasse entstammt.

Praktisch sind diese inneren Klassen, wenn man mit einem Objekt zwei verschiedene Interfaces befriedigen will. Es kann auch so etwas banales sein wie ein Collection, die mit getImmutable() eine Variante von sich selbst herausgibt, wo alle Methoden, die zu Änderungen führen würden, UnsupportedOperationException werfen.

Aber wie kommt man damit zu einem Ersatz für die Closures?

Vielleicht so:

public interface Function {
  public Y f(X x);
}

und dann kann man so etwas machen wie

public class C {
  public Function createAdder(final int x) {
    return new Function() {
      public Integer f(Integer xx) {
        return x+xx;
      }
    };
  }
  
  
  public static void main(String args[]) {
    C c = new C();
    Function ff = c.createAdder(3);
    for (int i = 0; i < 3; i++) {       System.out.println("i=" + i + " s=" + ff.f(i));     }     System.out.println();     ff = c.createAdder(90);     for (int i = 0; i < 3; i++) {       System.out.println("i=" + i + " s=" + ff.f(i));     }   } }

Das erzeugt etwa diese Ausgabe:

i=0 s=3
i=1 s=4
i=2 s=5

i=0 s=90
i=1 s=91
i=2 s=92

Diese Quelltexte kann man auf Github finden.

Es kommt also dem nahe, was man an dieser Stelle mit Perl machen kann.

Mit den Lambda-Funktionen oder Closures aus Java 8 sieht es dann etwa so aus:

  public Function createAdder(final int x) {
    return (xx) -> x+xx;
  }

Offensichtlich hat man dadurch also eine kürzere Schreibweise gewonnen, was Konstrukte dieser Art viel attraktiver macht.

Share Button

Closures I (Perl)

Alle „coolen“ Programmiersprachen haben sogenannte Closures. Java ist nicht cool, deshalb braucht man das dort nicht… 😉 Aber bitte bis zum Schluss weiterlesen…

Zunächst gibt es um diesen Begriff eine gewisse Verwirrung. Gemeint werden damit oft anonyme Funktionen, die man in einem Programm herumreichen kann, wie andere Werte und Objekte. Das ist so etwas, was man von funktionalen Programmiersprachen erwarten kann, auch wenn es noch nicht ausreicht, um eine Programmiersprache funktional zu machen. Die Funktionspointer in C sind etwas weniger als das, weil sie ja eine statisch definierte Funktion beschreiben, die erst im Kontext der Funktion, der sie als Parameter oder Variable sichtbar gemacht werden, anonym wirken. Andere Programmiersprachen wie Ruby, Perl, JavaScript, die meisten Lisp-Dialekte einschließlich Clojure, Scala, und viele andere mehr, in neueren Versionen wohl sogar C# und Java, haben solche anonymen Funktionen.

Zur Closure werden sie aber erst durch die Einbindung von Variablen aus dem Kontext, in dem sie definiert werden.
Hier wird etwas darüber geschrieben, wie Closures in Ruby funktionieren:
Closures in Ruby

Doch nun zu Perl. In diesem Zusammenhang muss man darauf achten, ob die lokalen Variablen mit „my“ oder mit „local“ deklariert.
In der Regel ist „my“ das, was man verwenden sollte und in der Regel macht es keinen Unterschied. Aber in diesem Fall sieht man es: Variablen, die mit my deklariert wurden, werden im definierenden Kontext der anonymen Funktion eingebunden, solche, die mit local deklariert wurden, im aufrufenden Kontext. Es gibt auch noch „our“, wobei der Unterschied zu „my“ in diesem Blogbeitrag erklärt ist. In diesem Beispiel verhält sich „our“ genauso wie „my“.

Hier ein Beispiel für die Verwendung von „my“:

#!/usr/bin/perl

# 2nd-order function, returns a reference to a function
sub createAdder($) {
    # local variable, will be accessed by anonymous function (early binding)
    my $x = $_[0];

    # define anonymous function, obtain reference to it and assign it to result
    my $result = ( sub {
                       # @_ refers to parameters of anonymous function
                       my $y = $_[0];
                       # $x comes from createAdder, $y comes from context of caller and $z is parameter of function
                       return ($x, $y, $x+$y);
                   });
    return $result;
}

# local variable, will not be accessed by called anonymous function
my $x = 100;

my $px = 30;

# obtain reference to function
my $ff = createAdder($px);

# call function
for (my $i = 0 ; $i < 3; $i++) {     my @arr = $ff->($i);
    my $arr_str = "( " . join(", ", @arr) . " )";
    print "x=$x i=$i px=30 (px, y, px+i)=", $arr_str , "\n";
}

print "\n";

$x = 1000;
$px = 60;

my $ff = createAdder(60);

# call function
for (my $i = 0 ; $i < 3; $i++) {     my @arr = $ff->($i);
    my $arr_str = "( " . join(", ", @arr) . " )";
    print "x=$x i=$i px=60 (px, i, px+i)=", $arr_str , "\n";
}

Man sieht also, dass das $x aus createAdder() in die anonyme Funktion eingebaut wird und es kommt diese Ausgabe:

x=100 i=0 px=30 (px, y, px+i)=( 30, 0, 30 )
x=100 i=1 px=30 (px, y, px+i)=( 30, 1, 31 )
x=100 i=2 px=30 (px, y, px+i)=( 30, 2, 32 )

x=1000 i=0 px=60 (px, i, px+i)=( 60, 0, 60 )
x=1000 i=1 px=60 (px, i, px+i)=( 60, 1, 61 )
x=1000 i=2 px=60 (px, i, px+i)=( 60, 2, 62 )

Mit local sieht es so aus:

#!/usr/bin/perl

# 2nd-order function, returns a reference to a function
sub createAdder($) {
    # local variable, will be ignored by anonymous function (late binding)
    local $x = $_[1];

    # define anonymous function, obtain reference to it and assign it to result
    my $result = ( sub {
                       # @_ refers to parameters of anonymous function
                       my $y = $_[0];
                       # $x comes from createAdder, $y comes from context of caller and $z is parameter of function
                       return ($x, $y, $x+$y);
                   });
    return $result;
}

# local variable, will be accessed as $y by anonymous function
local $x = 100;

local $px = 30;

# obtain reference to function
my $ff = createAdder($px);

# call function
for (my $i = 0 ; $i < 3; $i++) {     my @arr = $ff->($i);
    my $arr_str = "( " . join(", ", @arr) . " )";
    print "x=$x i=$i px=30 (px, y, px+i)=", $arr_str , "\n";
}

print "\n";

$x = 1000;
$px = 60;

$ff = createAdder(60);

# call function
for (my $i = 0 ; $i < 3; $i++) {     my @arr = $ff->($i);
    my $arr_str = "( " . join(", ", @arr) . " )";
    print "x=$x i=$i px=60 (px, i, px+i)=", $arr_str , "\n";
}

Man sieht also, dass diesmal das $x aus dem aufrufenden Kontext in der anonyme Funktion verwendet wird und es kommt diese Ausgabe:

x=100 i=0 px=30 (px, y, px+i)=( 100, 0, 100 )
x=100 i=1 px=30 (px, y, px+i)=( 100, 1, 101 )
x=100 i=2 px=30 (px, y, px+i)=( 100, 2, 102 )

x=1000 i=0 px=60 (px, i, px+i)=( 1000, 0, 1000 )
x=1000 i=1 px=60 (px, i, px+i)=( 1000, 1, 1001 )
x=1000 i=2 px=60 (px, i, px+i)=( 1000, 2, 1002 )

Diese Programme haben jeweils eine Funktion 2. Ordnung, createAdder(), die selbst eine Funktion zurückgibt.

Nun stellt sich aber heraus, dass Java ein sehr ähnliches, natürlich einmal wieder etwas umständlicheres Konstrukt hat. Das sind anonyme innere Klassen, die ein Interface mit genau einer Methode implementieren. Und Java 8 hat die Closures jetzt auch ohne diesen Umweg. Aber das ist sicher einmal ein anderer Blog-Beitrag.

Links

Share Button

Virtuellen Speicher überbelegen

So etwas müsste ja offensichtlich nicht gehen, man kann ja nur das belegen, was man hat…

Ich schreibe hier mal wieder mit Blick auf die Linux-Speicherverwaltung, die ich am besten kenne, aber die Ideen stammen teilweise von früheren Unix-Systemen.

Ein System hat einen physikalischen Speicher (RAM) von einer bestimmten Größe, heute meistens ein paar Gigabyte, aber mein erstes Linux lief in den frühen 90er-Jahren auch mit 4 Megabyte irgendwie. Oft wird das noch ergänzt durch einen sogenannten Swap- oder Paging-Bereich. Das bedeutet, dass man mehr Speicher belegen kann, als da ist und der weniger genutzte Bereich wird auf die Festplatte ausgelagert. Im Falle von ausführbaren Programmen reicht es dafür oft, diese zu memory-mappen, das heißt, dass man die Stelle auf der Platte, wo das Binary steht, verwendet und von dem Programm jeweils nur die aktuell benötigten Teile in den Hauptspeicher lädt, statt das im Swap-Bereich zu duplizieren.

Schön ist es immer, wenn man diesen Swap-Bereich nicht braucht, denn dann läuft alles schneller und RAM ist ja so billig geworden. Gerade wenn man nur SSDs hat, ist das durchaus eine valable Überlegung, weil die SSDs nicht so viel billiger als RAM sind und weil die SSDs durch das besonders häufige Schreiben bei Verwendung als Swap relativ schnell aufgebraucht werden. Mit guten SSDs sollte das durchaus noch brauchbar funktionieren, weil deren Lebensdauer so gestiegen ist, aber dafür muss man schon genau schauen, dass man solche mit vielen Schreibzyklen und einer hohen Schreibgeschwindigkeit kauft und die sind dann wirklich nicht mehr so billig.

Letztlich funktioniert es aber mit magnetischen Festplatten oder mit SSD prinzipiell genauso, solange die Hardware mitmacht. Einen Teil des belegten Speichers im Swap zu haben, ist nicht unbedingt falsch, denn viele Programme belegen Speicherbereiche und benutzen die nur sehr selten. Und weil in der Zeit, wo Speicherbereiche in den Swap-Bereich ein- und ausgelagert werden, andere Threads noch weiterrechnen können, kann es durchaus auch sein, dass man mit einem Teil der häufiger gebrauchten Speicherbereiche im Swap noch eine passable Performance erreichen kann. Das ist vor allem für seltener gebrauchte oft Software vertretbar, wobei man aber den Anwendungsfall genau anschauen muss. Zum Beispiel muss eine aufwendige Jahresendverarbeitung gut geplant werden, weil sie zu einem bestimmten Zeitpunkt abgeschlossen sein soll und vielleicht besonders viele Ressourcen benötigt.

Den Swap-Bereich und den physikalischen Speicher zusammen nennt man virtuellen Speicher. Von diesem wird noch ein Teil durch das Betriebssystem belegt, so dass man den Rest zur Verfügung hat. Nun kann man unter Linux diesen virtuellen Speicher tatsächlich überbelegen. Das nennt sich „overcommit“. Dabei wird in den Kernel-Einstellungen ein Parameter entsprechend gesetzt.

Man kann abfragen, welche overcommit-Strategie eingeschaltet ist:

$ cat /proc/sys/vm/overcommit_memory
0

0 bedeutet, dass nur Speicheranforderungen, die nach einer Heuristik plausibel ist, erlaubt wird, 1 bedeutet, dass alle Speicheranforderungen grundsätzlich erlaubt werden.

$ cat /proc/sys/vm/overcommit_ratio
50

bedeutet, dass eine Überbelegung von 50%, also ein Faktor von bis zu 1.5 möglich ist.
Mit sysctl als root oder durch schreiben in diese Pseudo-Dateien kann man das einmalig ändern, mit Einträgen in /etc/sysctl.conf für die Zukunft ab dem nächsten reboot.

Ein paar Fragen, die sich aufdrängen sind:

  • Wie kann das überhaupt funktionieren?
  • Wofür braucht man das?
  • Was sind die Risiken?

Es gibt verschiedene Möglichkeiten, Speicher anzufordern, aber es reduziert sich im wesentlichen auf malloc und Konsorten, fork und exec.

malloc(), calloc(), sbrk(), realloc() und einige andere fordern einen Speicherbereich an, den ein Programm benutzen möchte. Solange da nichts reingeschrieben wurde, brauchen diese Bereiche aber nicht wirklich zur Verfügung gestellt werden. Erst wenn tatsächlich die Zugriffe darauf erfolgen, ist das nötig. Das soll von älteren Fortran-Versionen kommen. Dort hat man Matrizen und Vektoren statisch allozieren müssen, es musste also im Programm hardcodiert sein, wie groß sie sind. Für kleinere Matrizen hat man einfach nur ein Rechteck in der linken oberen Ecke benutzt. So haben Fortran-Programme früher viel mehr Speicher angefordert, als sie wirklich brauchten und RAM war damals sehr teuer und auch der Plattenplatz für Swap war teuer. So konnte man mit entsprechender Einschätzung der Charakteristik der laufenden Programme mit overcommit das System tunen und mehr damit machen. Auch heute dürften noch viele Programme Speicher allozieren, den sie nicht brauchen, auch wenn das sich heute leichter umgehen ließe als mit Fortran66. Ein spezieller Fall sind Java-Programme, die einen recht großen, konfigurierbaren Speicherbereich anfordern und den dann weitgehend selber verwalten.

Der andere Fall ist das Starten von Prozessen. Ein neuer Prozess braucht Speicher. Nun wird unter Linux ein Prozess in zwei Schritten gestartet. Mit fork() wird ein laufender Prozess dupliziert. Wenn man also von einem großen Prozess aus ein fork() aufruft, braucht man potentiell sehr viel memory. Das wird aber nicht so heiß gegessen wie gekocht. Denn Linux kennt einen Mechanismus „copy on write“, das heißt, die Speicherbereiche werden von beiden Prozessen gemeinsam benutzt, bis einer der Prozesse dort etwas schreibt. Das darf der andere nicht sehen, weshalb der eine Speicherblock vorher dupliziert werden muss, so dass jeder Prozess seinen eigenen hat. fork() lädt also durchaus dazu ein, mit overcommit verwendet zu werden. Um nun einen anderen Prozess zu starten, wird exec() aufgerufen. Dann wird ein neues Programm im laufenden Prozess ausgeführt und dabei fast der gesamte Speicher des Prozesses neu initialisiert. Das kann Memory freigeben oder zusätzlich in Anspruch nehmen. Erhalten bleiben bei exec() zum Beispiel die Umgebungsvariablen und die offenen Dateien und Verbindungen.

Wenn nun aber das ganze angeforderte Memory den virtuellen Speicher überschreitet und auch tatsächlich verwendet wird? Dann tritt der out-of-memory-Killer auf den Plan und beendet einige Prozesse, bis der Speicher wieder ausreicht. Es ist also wichtig, dass der Systemadministrator seine Systeme kennt und beobachtet, wenn diese nicht krass überdimensioniert sind. So oder so führt es zu Problemen, wenn man den virtuellen Speicher vollständig ausschöpft, weil viele Programme bei scheiternden mallocs(), forks() u.s.w. auch nicht mehr korrekt funktionieren können. Damit meine ich nicht unbedingt, dass man sich jeden Tag auf allen Systemen einloggt, um zu schauen, wie es denen geht, sondern eher, dass man Skripte oder Werkzeuge verwendet, um die Daten zu erfassen und zu aggregieren.

Ich habe einmal für ein paar Monate etwa 1’000 Rechner administriert. Wenn man sich in jeden einloggen muss und dabei pro Rechner etwa 10 min verbringt, sind das 10’000 Minuten, also etwa 167 Stunden. Man wäre allein damit also einen Monat lang voll beschäftigt. Benötigt wurden weitgehende Automatisierungen, die es ermöglichten, die Aufgabe in etwa 20 Stunden pro Woche zu bewältigen. Ja, es waren eine sehr homogene Rechnerlandschaft, sonst wäre der Aufwand sicher größer gewesen.

Share Button

Collections und Multithreading

Am Beispiel von Java soll hier etwas geschrieben werden, was viele Programmiersprachen betrifft, auch wenn die funktionalen Sprachen eine gewisse Immunität gegen derartige Probleme versprechen.

Es geht um Klassen, die sogenannte Collections enthalten. Nun kann man diese mit den sogenannten Gettern herausgeben lassen und vielleicht sogar mittels Settern austauschen oder ändern.

Eine naïve Implementierung sieht etwa so aus:

import java.util.List;
import java.util.ArrayList;

public class C {
private final List l;

public C() {
this.l = new ArrayList();
}
public C(List l) {
this.l = l;
}

public List getL() {
return l;
}
....
}

Das sieht schön aus, ist aber recht gefährlich. Im Zusammenhang mit Multithreading kann es besonders schwierig werden.

Das Problem dabei ist, dass der Konstruktor eine Liste übergeben bekommt. Diese wird nicht kopiert, sondern nur referenziert. Nun weiß man nicht genau, was der Programmteil, der den Konstruktor aufgerufen hat, sonst noch mit der Liste macht. So kann sich diese verändern, ohne dass bei C selbst irgendwelche Manipulationen gemacht wurden. Mit den Gettern wird die Liste wiederum an weitere Programmteile verfügbar gemacht, die alle darin etwas ändern können. Vielleicht sogar in mehreren Threads gleichzeitig, was sehr schnell zu merkwürdigen Exceptions führt, wenn man nicht speziell dafür ausgelegte Collections verwendet.

Wie kann man das lösen?

Oft hilft es, im Konstruktor die Liste zu kopieren und beim getter als immutable herauszugeben:

import java.util.List;
import java.util.ArrayList;
import java.util.Collections;

public class C {
private final List l;

public C() {
this.l = new ArrayList();
}
public C(List l) {
this.l = new ArrayList(l);
}

public List getL() {
return Collections.unmodifiableList(l);
}
....
}

Damit sind diese beiden Probleme abgestellt, zumindest in diesem Fall. Wenn es Listen von Listen oder andere kompliziertere Strukturen sind, dann treten die beschriebenen Probleme wegen der Unterlisten wieder auf. Hier sind die Elemente aber vom Typ String und damit selbst unveränderlich. Interessant ist nur die Frage, was passiert, wenn Methoden der Klasse C die darin gespeicherte Liste manipulieren. Die Programmteile, die vorher den getter aufgerufen haben und die dabei erhaltene Liste gespeichert haben, bekommen die Änderungen mit. Das kann erwünscht sein. Man kann es aber auch vermeiden, indem man beim getter noch einmal kopiert. Man kann sich das ewige kopieren auch sparen, indem man die Kopie „casht“ und wegwirft, sobald sich am Original etwas geändert hat.


import java.util.List;
import java.util.ArrayList;
import java.util.Collections;

public class C {
private final List l;
private transient List ll = null;

public C() {
this.l = new ArrayList();
}
public C(List l) {
this.l = new ArrayList(l);
}

public synchronized List getL() {
if (ll == null) {
ll = Collections.unmodifiableList(new ArrayList(l));
}
return ll;
}

public synchronized void changeL() {
ll = null;
l.add("x" + System.currentTimeMillis());
}
}

Der Nachteil ist natürlich, dass dieses viele Kopieren Zeit kostet und dann auch noch den Garbage-Collector beschäftigt.

Wie sieht es nun in funktionalen Sprachen aus? Typischerweise sind dort solche Collections auch immutable, zumindest gibt es diese Variante. Jede Manipulation an der Collection lässt diese selbst unverändert und gibt eine Kopie zurück, die die Änderung enthält. Das ist natürlich auch auf den ersten Blick ineffizient, weil man zum Beispiel von einer Liste immer größere Kopien machen muss, um nur jeweils ein Element hinzuzufügen. Das wird aber nicht so heiß gegessen wie gekocht. Man kann eine Liste implementieren, die sich so verhält, als würde man jedes Mal eine Kopie anlegen. In Wirklichkeit kann man intern einige Optimierungen vornehmen, wenn zum Beispiel ein Zwischenzustand dieser Operationen nie weiterverwendet wird. So zeigt es sich, dass funktionale Programmiersprachen trotz der scheinbar ineffizienten Kopiererei sehr effizient sein können.

Share Button

Steckdosen und Stromkabel der Zukunft

Heute haben wir in der Welt etwa 10-15 verschiedene gängige Steckdosentypen für Haushaltsgebrauch. Dabei übertragen diese nur Energie. Für Datenübertragung muß man einen zweiten Kanal haben, sei es ein zweites Kabel oder Funk oder so eine „powerline“-Technik, die das Stromkabel mit Hochfrequenzen für die Datenübertragung nutzt. In den Zeiten, wo unsere Haushalte Stromkabel bekommen haben, schien das ausreichend und man hatte völlig getrennt vom Stromkabel die Telefonkabel zum Kommunizieren. Immerhin bekamen die Telefone von dort ihre Energie, so dass man auch bei Stromausfall noch telefonieren konnte.

Nun gibt es aber interessante Anwendungsmöglichkeiten für einen Kommunikationskanal in vielen Haushaltsgeräte. Nicht nur das Display mit der Website für Rezepte auf dem Kühlschrank, an das viele dabei denken, sondern insbesondere auch zur Optimierung des Stromverbrauchs. Heutige Stromtarife haben einen festen Preis pro kWh, vielleicht einen zweiten für die Nacht. Das Stromnetz ist aber dynamisch, es gibt Nachfrage- und Angebotsschwankungen, die sich nicht an feste Zeitraster von Tages- und Nachtstrom halten. Im Sinne einer effizienteren Nutzung wäre es also sinnvoll, wenn man bei verschiedenen Geräten den momentanen Strompreis zur Verfügung hätte und die entsprechenden Aktionen zeitlich etwas versetzt durchführen könnte. Beispielsweise läuft der Kompressor in einem Kühlschrank nur einen Teil der Zeit und er schaltet sich ein, wenn die Temperatur zu hoch wird. Nun könnte man einen Toleranzspanne festlegen und je nach momentanem Strompreis würde er sich am unteren oder am oberen Ende dieses Temperaturintervalls einschalten. Bei Waschmaschinen und Ladegeräten für Mobiltelefone könnte man zwischen einem „Express-Modus“, der die Aufgabe so schnell wie möglich erfüllt, und einem Sparmodus, der vielleicht 1.5 Mal so lange braucht, aber billigeren Strom nutzt, wählen. Das ist natürlich anspruchsvoll, denn man will z.B. die Akku-Lebensdauer nicht durch so ein komisches Laden verkürzen. Aber vielleicht lösbar. Aber es gibt noch sehr viel mehr Möglichkeiten, wenn jedes Elektrogerät prinzipiell Netzwerk zur Verfügung hat.

Die richtige Lösung ist eigentlich, dass Stromkabel und Steckdosen nicht 3-polig, sondern vielleicht 6-polig sind, mit 2-4 zusätzlichen dünnen Drähten für eine einfache Datenübertragung, die man beim einstöpseln des Steckers automatisch mitverbindet. Die Idee stammt aus den frühen 80er Jahren, aber sie ist auch heute noch sinnvoll. Wenn die meisten Geräte so einen Netzwerkanschluss brauchen, ist das die sinnvollste Lösung.

  • separates Ethernetkabel ist zu umständlich und zu viel Kabelsalat
  • Funknetze sind zwar elegant, aber sie brauchen doch unnötig Ressourcen und man könnte dieselbe Bequemlichkeit haben, wenn der Stecker das Netzwerk enthält. Dann wäre die Kapazität der Funkkanäle für die wirklich mobilen Geräte da.
  • Powerline sieht interessant aus, aber man macht doch die Qualität der Sinuswelle im Stromnetz dadurch kaputt. Ich glaube, dass das nicht die sauberste Lösung ist, sondern mehr oder weniger Kompromisse an an verschiedenen Stellen bedingt.
  • Powerline und Funknetz erfordern aufwendigere Technologie und womöglich Konfiguration an allen Endgeräten, was die Sache teurer und unpraktischer macht.

Für Computer, insbesondere Server oder Desktoprechner mit großem Bandbreitenhunger halte ich auch in Zukunft ein separates kabelgebundenes Netzwerk für sinnvoll. Das betrifft aber nur einen kleinen Anteil der Elektrogeräte. Hier ist es aber vorteilhaft, Innovationen bei der Netzwerktechnologie innerhalb weniger Jahre umsetzen zu können, während das Netzwerk in der Steckdose standardisiert und für Jahrzehnte festgelegt bleiben muss. Trotzdem könnte es für einfache Anwendungen wie z.B. EMails lesen, ausreichen.

Vielleicht gibt es irgendwann einmal einen ISO-Stecker, der nach und nach in allen Ländern das gute Dutzend verschiedener Stecker verdrängt und der auch gleich noch eine einfache Internetverbindung beinhaltet.

Share Button

Das Richtige entwickeln – richtig gefragt

Wenn eine neue Software entwickelt oder erweitert wird, ist es ja immer eine wichtige Frage, was eigentlich entwickelt werden soll. Die Entwickler wissen es selten selbst, und auch die Kunden oder die Besteller oder die Nutzer der Software muss man gelegentlich erst ein Stück weit begleiten, bis man herausbekommt, was sie wirklich wollen und benötigen. Requirements-Engineering oder Anforderungsanalyse ist eine anspruchsvolle und wichtige Aufgabe. Oft gibt es eine sogenannte „Fachseite“, die in Zusammenarbeit mit Businessanalysten diese Anforderungen definiert.

Im reinen Wasserfallmodell hat man diesen Schritt am Anfang sehr vollständig durchgezogen und dann später auf dieser Basis die Software entwickelt, zu Korrekturzwecken gab es immerhin Änderungsanträge. Bei agileren Prozessen entwickeln sich die Details der Anforderungen oft noch während der Entwicklung weiter und solange der Teil, der gerade entwickelt wird, klar genug spezifiziert werden kann, hat man so die Möglichkeit, aufgrund der Erkenntnisse aus früheren Lieferungen etwas für die später möglichen Anforderungen zu lernen. Man sagt, dass der Appetit mit dem Essen kommt.

Es lohnt sich aber in jedem Fall, noch einen Schritt weiter zu denken. Es kommt auch oft genug vor, dass die Anforderungen durchaus schon gut genug bekannt sind. Nun ist die Erfahrung aber die, dass die Software am Ende nur 80% von dem kann, was man sich eigentlich erhofft hat und so werden die Anforderungen sicherheitshalber etwas höher gestellt, in der Hoffnung, dass von der wirklich gewollten Funktionalität dann am Ende genug da ist. Außerdem kommt es oft vor, dass Anforderungen mit einer bestimmten Implementierung im Hinterkopf gestellt werden, die sich aber später auf Seite von Softwarearchitektur oder -entwicklung als suboptimal erweist.

Hier lohnt es sich, miteinander zu reden, um die wirklichen Anforderungen in Erfahrung zu bringen. Oft stellt sich dann heraus, dass eine wesentlich robustere Implementierung möglich ist, die den eigentlichen Anforderungen, nicht aber den kommunizierten Anforderungen, gleich gut oder sogar besser genügt.

Ein Klassiker sind Tabellen, zum Beispiel für ein Gebührenmodell oder Steuern. Natürlich kann man Tabellen in Software umsetzen, man kann auch eine Möglichkeit anbieten, sie zu ändern. Diese Tabellen können aber recht groß werden und auch wenn das unter Aspekten wie Speicherverbrauch, Verarbeitungsgeschwindigkeit und Durchsatz heute in der Regel keine Rolle spielt, müssen diese Tabellen doch aktuell gehalten werden, was den Prozess zum Betrieb der Software erschwert, vor allem, wenn die Änderung der Tabellen einen Software-Update erfordern, weil sie hardcodiert sind.

Bei genauerem Nachfragen stellt sich heraus, dass die Tabelle eigentlich aufgrund einer Formel gebildet wurde und diese approximiert. Wenn man nun die Formel in die Software einbaut und vielleicht ermöglicht, dass 2-3 Parameter dazu geändert werden kann, bekommt man eine Lösung die „glatter“ ist, weil es keine solchen Sprünge mehr zwischen 4999 und 5000 gibt, was man natürlich als Vorteil erst einmal verkaufen muss, man bekommt eine besser wartbare Software, weil sie viel weniger Code braucht und weniger kaputt gehen kann, aber vor allem bekommt man im Betrieb einen Vorteil, weil man nur noch 2-3 Parameter der Formel aktuell halten muss und nicht mehr eine riesige Tabelle. Dass sich der Softwaretest vereinfacht, ist ein weiterer Vorteil.

Ein konkreter Fall betraf vor vielen Jahren eine Software für die Verarbeitung von Fahrplänen eines Verkehrsbetriebs. Das Zielsystem war für reguläre Schweizer Fahrpläne entwickelt worden, und man konnte so Linien, Routen, das sind verschiedene Fahrtverläufe innerhalb der Linie, mindestens zwei für die beiden Fahrtrichtungen, und ein gutes Dutzend Fahrzeitmuster für Hauptverkehrszeit (langsam), Nacht (schnell) u.s.w. festlegen. Das passt für mitteleuropäischen Stadtverkehr überall sehr gut. Eine kanadische Stadt weist aber eine viel niedrigere Bevölkerungsdichte auf und der Anteil der öffentlichen Verkehrsmittel an den zurückgelegten Wegen ist auch typischerweise kleiner als in durchschnittlichen mitteleuropäischen Städten. So hat man etwas, was zwischen einem Überlandverkehr und Stadtverkehr in Mitteleuropa liegt. Das Budget für den Verkehrsbetrieb ist begrenzt und man bemüht sich, trotz allem noch eine regelmäßige Bedienung der Stadtregionen zu bewerkstelligen. Dafür war eine Fahrplanerstellungssoftware vorteilhaft, die die Fahrzeiten individuell pro Fahrt wählbar macht. Die Sprachbarriere kam noch hinzu und so meinte der Schweizer Lieferant, die Kanadier sollten nur ihre Fahrplan ein bisschen aufräumen, dann würde es schon passen und die Kanadier meinten, sie wollten sich ihre gute Arbeit nicht wegen einer restriktiven Software verhindern lassen. Als das einige Zeit nach Projektbeginn festgestellt wurde, gab es erst einmal einen Scherbenhaufen.

Die Lösung war schließlich, noch einmal genau die Bedürfnisse des Kunden anzuschauen und konkret die kompliziertesten Linien unter die Lupe zu nehmen. Nun sollte die Software eine bestimmte Zuverlässigkeit bei der Verwendung der Daten erzielen. Es stellte sich am Ende heraus, dass das alles lösbar war. Hierzu musste man die Fahrzeitmuster der einzelnen Fahrten ermitteln und jeweils die häufigsten Muster in die Zielplattform übernehmen. Bei den meisten Linien reichte das aus, aber für einzelne Fahrten musste das passendste unter den übernommenen Fahrzeitmustern verwendet werden, was zu einem Fehler führte, der aber noch in die Toleranz für das Gesamtsystem fiel. Die richtige Lösung, das Zielsystem zu erweitern, um die Daten vollständig aufzunehmen, sollte man natürlich nicht aus dem Auge verlieren, was aber ein längerfristiges Projekt war. Mit so einer Kompromisslösung ließ sich dafür etwas Zeit gewinnen und letztlich die unter den gegebenen Umständen bestmögliche Lösung für alle beteiligten umsetzen.

Man sieht also, es lohnt sich auf jeden Fall, genauere Fragen zu stellen, um unter Berücksichtigung der Möglichkeiten, die man von der Softwarearchitektur im gegebenen Zeit- und Budgetrahmen hat, die wirklichen Bedürfnisse möglichst zielgerichtet zu erfüllen.

Share Button

Five Programming Languages you should learn

Larry Wall talking about his suggestion for five programming languages one should know:

youtube

Share Button

Warum Baumstruktur

Für Dateiverzeichnisse (Filesysteme) hat sich eine Baumstruktur etabliert. Wir haben uns daran gewöhnt und für die meisten Zwecke ist das auch eine sinnvolle Strukturierung.

Relativ oft wird man aber mit dem Problem konfrontiert, dass es zwei oder mehr Aspekte gibt, nach denen man seine Dateien oder Unterverzeichnisse gruppieren will. Machen wir es mal abstrakt mit Farbe, Form und Größe.

Dann kann man etwa so etwas haben wie

$MAINDIR/rot
$MAINDIR/blau
$MAINDIR/gruen
$MAINDIR/gelb

und in der nächsten Ebene dann jeweils so etwas wie

$MAINDIR/rot/quadrat
$MAINDIR/rot/kreis
$MAINDIR/rot/dreieck
$MAINDIR/rot/sechseck
$MAINDIR/blau/quadrat
$MAINDIR/blau/kreis
...
$MAINDIR/gelb/dreieck
$MAINDIR/gelb/sechseck

und darunter jeweils

.../klein
.../mittel
.../gross

Aber manchmal möchte man es genau anders haben haben, also zuerst nach Form differenzieren, dann nach Größe und zuletzt nach Farbe.
Und es kann oft sehr unpraktisch sein, wenn Dinge, die eigentlich zusammengehören und die man zusammen bearbeiten will, verstreut über das Verzeichnissystem herumliegen.

Es hilft sicher, dass man sich vorher gut überlegt, wie man seine Verzeichnisstruktur aufbaut, so dass es zumindest in möglichst vielen Situationen einigermaßen passt. Es gibt sicher Möglichkeiten, damit umzugehen. Man könnte zum Beispiel alle sechs Verzeichnisbäume anlegen und via Hardlinks oder Softlinks die Dateien, die man im ersten Verzeichnisbaum richtig eingetragen hat, von den anderen Verzeichnisbäumen jeweils passend verlinken (Softlink) oder dort auch eintragen (Hardlink). Das geht unter Linux und Unix mit deren eigenen Dateisystemen gut, es kann aber eine Herausforderung sein, das konsistent zu halten.

Man kann auch die Daten auf Dateisystemebene irgendwie ablegen, vielleicht sogar in einer der möglichen Strukturierungen, aber den Zugriff über eine Applikation anbieten, die es ermöglicht, so darauf zuzugreifen, als wären sie in einer der anderen fünf Arten strukturiert. Ein typisches Beispiel sind Musikprogramme (Player), die es ermöglichen, die Musikstücke nach Genre, Sänger, und vielen anderen Kriterien zu strukturieren und dann mit diesen Daten darauf zuzugreifen. Es gibt dann typischerweise irgendwo Metadaten, die diese Zusatzinformationen enthalten. Hier hat man noch den Zugriff über das Dateisystem, was sehr praktisch ist, gerade bei Musik, aber die Applikation muss Änderungen erkennen und gegebenenfalls die Metadaten anpassen oder sogar interaktiv abfragen und ergänzen.

Noch weiter geht es, wenn die Datenspeicherung völlig intransparent ist, im Extremfall sogar unter Benutzung sogenannter „Raw-Devices“, und wenn man dann nur noch über so eine Applikation an die Daten herankommt. Im Grunde genommen sind alle Datenbanken in dieser Rubrik angesiedelt, aber auch die Repositories vieler Versionsverwaltungssysteme oder auch Content-Management-Systeme, die natürlich wiederum eine Datenbank verwenden können. Eine Folge davon ist, dass man an diese Daten nur noch mit der Applikation oder deren Hilfsprogrammen herankommt und nicht mehr wirklich auf Dateisystemebene damit arbeiten kann. Eine kleine Anzahl von Datenbankprodukten und das zugehörige Knowhow leistet man sich gerne, denn das braucht man und es lohnt sich. Aber wenn jetzt Software unnötigerweise ihr eigenes Repository-Format statt einer einfachen Dateispeicherung verwendet, kann es schon schwieriger werden, die ganzen Daten im Griff zu behalten.

Das Problem ist also in der Praxis dort, wo es eine große Rolle spielt, häufig irgendwie gelöst. Trotzdem wäre es manchmal schön, wenn die Verzeichnisstruktur nicht wie ein Verzeichnisbaum aufgebaut wäre, sondern zumindst in manchen Teilbäumen wie ein dünn besetzter Hyperquader, wo man nach einer Reihe von Kriterien die passenden Dateien suchen und in einem temporären virtuellen Verzeichnisbaum zugänglich machen könnte. Diese Mehrfachindizierung war übrigens auch in Datenbanken vor etwa 30 Jahren eine Herausforderung, wenn die einzelnen Indizes für sich genommen nicht selektiv genug sind. Man hat wohl das Konzept des Z-Index (oder der Z-Kurve) entwickelt, womit eine Verflechtung von verschiedenen Indizes möglich ist, die für sich genommen nicht selektiv genug sind, um für einen bestimmten Indexwert nur noch eine so kleine Menge von Daten zurückzuliefern, dass eine erschöpfende Suche in diesen Daten unproblematisch wäre.

In der Systemadministration stellt sich diese Frage häufig. In der Linux- und Unix-Welt ist es üblich, größere Applikationen so zu installieren, dass etwa 4-6 Orte im Verzeichnissystem verwendet werden:

  • ausführbare Programme in /usr/bin oder /usr/local/bin oder /bin o.ä.
  • Bibliotheken und statische Hilfsdaten dazu in /usr/lib oder /usr/local/lib oder /opt. Zum Teil architekturunabhängige statische Hilfsdaten und Bibliothken auch in /usr/share. Oft ist die ganze Applikation unter /opt installiert und es gibt softlinks nach /usr/local/bin
  • Man-Seiten unter /usr/man, /usr/local/man, /opt/man o.ä.
  • Konfiguration unter /etc
  • Variable Daten (z.B. Tablespaces für Datenbanken) unter /var/lib, /var/opt o.ä.
  • logs unter /var/log

Das ist das, was sich eigentlich etabliert hat und wie die Software, die mit Linux-Distributionen mitkommt, üblicherweise installiert wird. Man hat so nicht die ganze Applikation zusammen in einem Verzeichnis, aber dafür hat man eine Trennung in statische und dynamische Daten und kann auf Servern viel gezielter Entscheidungen darüber treffen, welche Daten in welches Backup kommen, welche Raid-Konfiguration man verwendet, ob man die Daten lokal oder remote hält, ob man sie mit anderen Servern teilen kann und ob man SSDs einsetzt oder auch wer Schreib- und Leseberechtigungen hat. Diese Fragen wird man eher für alle Daten in /var/lib gleich beantworten als für alle Daten unter /tomcat.
So wird die zweitbeste Vorgehensweise, eben alles, was zu einer Applikation gehört, komplett in einem Unterverzeichnis zu halten, trotz ihrer offensichtlichen Reize, eher selten gewählt. Hier wäre so eine Matrixstruktur für das Dateisystem schön, man könnte also Zeilen für die Applikationen haben und Spalten für die Art der Daten (lib, bin, share, var, log, conf, man,…).

Share Button

iO

Niemand liest Zeitungen wie „Blick“ und „Bild“, aber jeder weiß was drinsteht. Jedenfalls wenn man die Titelseite überall rumliegen sieht. Mal ein interessantes Informatik-Projekt, das dort auf die erste Seite geschafft hat, auch wenn es nur eine Zeitung ist, die niemand liest. Warum braucht man noch ein weiteres Skype oder Whatsapp? Das muß die Swisscom wohl selber wissen, aber nach PRSIM ist ein System, das von einer Schweizer Firma betrieben wird, einfach vertrauenswürdiger als eines von einer amerikanischen Firma.

Update 2019-03-23: Die Webseite https://io.swisscom.com/ ist nicht mehr vorhanden.

Share Button

Kompressionsprogramme

Jeder kennt diese Kompressionsprogramme wie gzip, arj, (win)zip, 7z und noch mehr. Sie sind praktisch, um Daten platzsparend aufzubewahren, bandbreitensparend zu übermitteln oder auch einfach nur um Daten zu einer Datei zusammenzupacken. Dabei hat die letzte Aufgabe eigentlich gar nicht viel mit Kompression zu tun, wird aber von manchen Werkzeugen zusätzlich zur Kompression auch noch unterstützt. Ein weniger bekannter Grund für Kompression, der aber recht wichtig ist, ist im Bereich der Kryptographie. Wenn man Daten vor der Verschlüsselung komprimiert, ist es schwieriger, den Schlüssel zu erraten.

Witzigerweise hat sich in der Linux-Welt als Archivformat das .tar.gz-Format weit verbreitet, während in der MS-Windows-Welt eher die zip-Dateien üblich sind, obwohl beide Formate auf beiden Plattformen relativ gut unterstützt werden:
Unter Linux kann man zwei Programme, zip und unzip finden oder einfach installieren, die ZIP-Dateien erzeugen und auspacken. Unter MS-Windows kann man z.B. cygwin installieren und hat dann auch tar und gzip dabei, oder man kann mit Winzip auch tar.gz-Dateien zumindest auspacken.

Hinter diesen beiden Formaten und den zugehörigen Werkzeugen stehen zwei verschiedene Ansätze. In der Linux-Welt und früher noch mehr in der Unix-Welt war es üblich, Werkzeuge zu haben, die eine Aufgabe sehr gut erfüllen und die sich mit anderen Werkzeugen kombinieren lassen. Herausgekommen ist gzip, das nur eine einzelne Datei oder einen Datenstrom komprimiert, nicht aber mehrere Dateien in ein Archiv zusammenführt, wofür andere Werkzeuge, z.B. tar herangezogen werden können. Man kann das aber mit einem Aufruf erledigen, indem man tar mit einer Option mitgibt, das das Archiv nach der Erstellung noch durch gzip geschickt werden soll oder vor dem Lesen durch gunzip. Dagegen mach zip alles in einem Werkzeug.

Bei genauerem Hinsehen gibt es aber noch eine subtilen, aber doch manchmal wichtigen Unterschied, der eigentlich wenig mit der Frage zu tun hat, ob man ein Werkzeug hat, das beide Aufgaben verbindet.

Im ZIP-Format werden die Dateien einzeln komprimiert und dann die komprimierten Dateien zu einem Archiv zusammengefügt. Bei .tar.gz ist es umgekehrt. Oft ist die Wahl durch Gewohnheiten oder durch Kommunikationspartner ein Stück weit eingeschränkt, aber es lohnt sich doch, diesen Unterschied zu kennen: Beide Ansätze haben ihre Vorteile in verschiedenen Situationen und deshalb ist es eigentlich gut, dass man auf beiden Plattformen auch beide verwenden kann.

Der Vorteil, erst zu archivieren und das Archiv zu komprimieren ist einfach, dass man dabei eine bessere Kompression erzielen kann. Da die Kompression ja darauf basiert, irgendwelche Gemeinsamkeiten, Regelmäßigkeiten oder allgemein Redundanzen von verschiedenen Sequenzen innerhalb der komprimierten Datei zu finden, ist es plausibel anzunehmen, dass man die Kompression manchmal verbessern kann, wenn dies über alle archivierten Dateien gemacht werden kann und nicht nur über die einzelnen archivierten Dateien. Als Beispiel habe ich einmal das /bin-Verzeichnis komprimiert:

$ ls -l bin.tar.gz bin.zip
-rw-r--r-- 1 bk1 brodowsky 3909375 2013-06-20 22:23 bin.tar.gz
-rw-r--r-- 1 bk1 brodowsky 7568659 2013-06-20 22:24 bin.zip

Man sieht also, dass die Wiederholungen und Ähnlichkeiten zwischen den Dateien dazu führen, dass trotz gleichem Kompressionsalgorithmus die tar.gz-Datei nur etwa halb so groß wie die zip-Datei ist.

Der Vorteil, erst zu komprimieren und dann die einzelnen komprimierten Dateien zu archivieren besteht darin, dass man leichter auf einzelne Dateien aus dem Archiv zugreifen kann. Bei tar.gz muss man alles vom Anfang bis zu der gesuchten Datei dekomprimieren, im Durchschnitt also etwas mehr als die Hälfte des Archivs, bei zip nur die eine Datei, die man wirklich will. Aus diesem Grunde wurde das zip-Format auch für die Programmiersprache java verwendet, um die Bibliotheken (jar-Dateien) zu speichern. Das sind ZIP-Dateien mit einem zusätzlichen Verzeichnis META-INF, das Metainformationen enthalten kann. Man kann jar-Dateien mit unzip auspacken und zip-Dateien mit jar xfvv.

Wenn man nun aber eine Datenbankspalte oder -tabelle komprimieren will, sind diese beiden Ansätze nicht so attraktiv. Im einen Fall werden die Zugriffe unakzeptabel langsam, im anderen Fall hat man mit einem einzelnen Datenbankattribut in einer einzelnen Zeile selten genug Masse, um von der Kompression profitieren zu können. Wie könnte man das trotzdem lösen? Vielleicht wäre eine Möglichkeit, dass man ab einer gewissen Datenmenge in der Spalte, also wenn es genug Zeilen gibt, bei denen dieses Attribut nicht null ist, die Daten analysiert und in einem mit der Tabelle assoziierten Bereich Metadaten zur Kompression ablegt, die etwa aus mehrfach vorkommenden Bit- oder Byte-Sequenzen bestehen und diese auf andere Bit- oder Byte-Sequenzen abbilden, so dass die häufigeren kürzer sind als die selteneren gleicher Originallänge. Solange die neu hinzukommenden Daten etwa den schon vorhandenen entsprechen, kann man sie damit auch komprimieren und tatsächlich die Datenbank kleiner machen, aber vor allem in manchen Fällen schneller und in Kombination mit Verschlüsselung vielleicht sogar sicherer.

Nebenbei bemerkt, dass es viele Werkzeuge gibt, die ähnlich wie die hier erwähnten Vertreter funktionieren:

  • lha, zoo und 7z funktionieren z.B. ähnlich wie zip.
  • compress, bzip2 und xz funktionieren ähnlich wie gzip
  • cpio funktioniert ähnlich wie tar

Weil compress in den 90er-Jahren auf patentierten Algorithmen basierte, hat man es praktisch vollständig durch gzip ersetzt, was auch die bessere Kompressionsraten auf Kosten des Zeitaufwands zum Komprimieren erzielt hat. Dann kam bzip2, das noch besser und noch langsamer komprimiert und xz, noch besser und noch langsamer.

tar hieß ursprünglich „tape archiver“, konnte als Bandlaufwerke ansprechen. Es wird aber seit 20 Jahren in meiner Erfahrung ganz überwiegend für Archivierung in Dateien verwendet und sehr selten für Bandlaufwerke. Dafür kam dann irgendwann cpio, das aber auch für beide Zwecke verwendbar ist, wie tar. Nur ist cpio weniger verbreitet und man muss wohl als durchschnittlicher Linux-Anwender häufiger die man-Seite anschauen, wenn man das verwendet, während man bei tar eher die wichtigsten Optionen auswendig kennen könnte.

Share Button