Zustandsmaschinen gegen Threads


24

Alan Cox sagte einmal : "Ein Computer ist eine Zustandsmaschine. Threads sind für Leute, die keine Zustandsmaschinen programmieren können."
Da es für mich keine Option ist, Alan direkt zu fragen, möchte ich hier lieber fragen: Wie erreicht man Multithreading-Funktionalität in Hochsprachen wie Java mit nur einem Thread und einer Zustandsmaschine? Was ist zum Beispiel, wenn zwei Aktivitäten ausgeführt werden müssen (Berechnungen ausführen und E / A ausführen) und eine Aktivität blockieren kann?
Ist die Verwendung von "state-machine only" eine sinnvolle Alternative zum Multithreading in Hochsprachen?


4
Ein Computer ist auch eine Turingmaschine. Trotzdem ist es nicht unbedingt sinnvoll, es wie eine Turing-Maschine zu programmieren. Der Stapel in imperativen Sprachen ist eine äußerst nützliche Sache, und Multithread-Programme ermöglichen es, mehrere Stapel gleichzeitig im Speicher zu halten. Dasselbe in einer Zustandsmaschine zu tun ist sicherlich möglich, aber umso chaotischer.
Thiton

10
Alan war ein OS-Kernel-Entwickler; das war sein domian . Also sollte sein Zitat in diesem Kontext gesehen werden. Er hätte "gegen das Metall" programmiert, wo es sinnvoller wäre, ein solches Modell zu verwenden. Sobald das Betriebssystem die Hardware und ihre eigentlichen Eigenschaften ("ein Computer ist eine Zustandsmaschine ...") entfernt hat, haben Sie die Möglichkeit und den Vorteil, andere Modelle verwenden zu können, die in Ihrer Domäne sinnvoller sind . Fast macht jedes Spiel schwer Einsatz von Zustandsmaschinen.
Steven Evers

2
Threading ist einfach eine Funktion des Betriebssystems, mit der Sie einige Ihrer Zustandsmaschinen-Switches automatisch verwalten können. Natürlich können Sie eine riesige Zustandsmaschine erstellen, die alles selbst verwaltet, aber das ist komplizierter. Gleiches gilt für Prozesse. Man kann sagen, dass Prozesse für Leute sind, die auch keine Zustandsmaschinen programmieren können. Die Abstraktion bietet Ihnen jedoch eine viel einfachere und weniger fehleranfällige Oberfläche. Meiner Meinung nach ist dies nur ein weiteres "cooles Zitat", das in der Realität gehört, in Betracht gezogen und dann ignoriert werden sollte.
Yam Marcovic

2
"Aber die [thread] -Abstraktion bietet Ihnen eine viel einfachere und weniger fehleranfällige Oberfläche." Das scheint falsch zu sein. Die Anzahl der Personen, bei denen die Threadsicherheit falsch ist, deutet darauf hin, dass dies Fehler verursacht.
S.Lott

1
Viele der Kommentare und Antworten hier interpretieren das Zitat als Anti-Multitasking im Allgemeinen. Ich glaube, Alan Cox ist nur ein Anti-Thread und würde die Verwendung mehrerer Prozesse empfehlen, um viele der Ziele zu erreichen, für die die Leute Threads verwenden. Denken Sie daran, er ist ein Unix-Hacker: Fork FTW. Ich habe keine Kommentare von ihm direkt zu dem Zitat gefunden, aber hier ist eine von Larry McVoy aus der Linux-Kernel-Mailingliste, die in diese Richtung geht: lkml.indiana.edu/hypermail/linux/kernel/0106.2/0405.html
Martin B

Antworten:


25

Alles, was ein Thread tut, sind Verschachtelungsoperationen, sodass Teile des Prozesses sich zeitlich zu überlappen scheinen. Ein Single-Core-Rechner mit mehreren Threads springt nur herum: Er führt kleine Codestücke von einem Thread aus und wechselt dann zu einem anderen Thread. Ein einfacher Scheduler entscheidet, welcher Thread die höchste Priorität hat und tatsächlich im Core ausgeführt wird.

Auf einem Single-Core-Computer passiert eigentlich nichts "zur gleichen Zeit". Es ist alles nur verschachtelte Ausführung.

Es gibt viele, viele Möglichkeiten, Interleaving zu erreichen. Viele.

Angenommen, Sie haben einen einfachen Prozess mit zwei Threads, der eine einfache Sperre verwendet, sodass beide Threads in eine gemeinsame Variable schreiben können. Sie haben sechs Codeblöcke.

  • T1-vor-Sperre
  • T1-mit Schloss
  • T1-After-Sperre
  • T2-vor-Sperre
  • T2-mit Schloss
  • T2-Nachlaufsperre

[Dies kann in einer Schleife sein oder mehrere Sperren haben oder was auch immer. Es wird nur länger, nicht komplexer.]

Die Schritte von T1 müssen der Reihe nach ablaufen (T1-vor, T1-mit, T1-nach) und die Schritte von T2 müssen der Reihe nach ablaufen (T2-vor, T2-mit, T2-nach).

Abgesehen von der "In-Order" -Einschränkung können diese in beliebiger Weise verschachtelt werden. Sowieso. Sie können wie oben aufgeführt ausgeführt werden. Eine andere gültige Reihenfolge ist (T1-vorher, T2-vorher, T2-sperren, T1-sperren, T2-nachher, T1-nachher). Es gibt viele gültige Bestellungen.

Warten.

Dies ist nur eine Zustandsmaschine mit sechs Zuständen.

Es handelt sich um nicht deterministische Automaten mit endlichen Zuständen. Die Reihenfolge von T1-xxx-Zuständen mit T2-xxx-Zuständen ist unbestimmt und spielt keine Rolle. Es gibt also Orte, an denen der "nächste Staat" ein Münzwurf ist.

Wenn der FSM beispielsweise gestartet wird, sind T1-vor oder T2-vor legitime erste Zustände. Wirf eine Münze.

Nehmen wir an, es ist T1-vorgekommen. TU das. Wenn dies erledigt ist, besteht die Wahl zwischen T1-with und T2-before. Wirf eine Münze.

Bei jedem Schritt in der FSM gibt es zwei Auswahlmöglichkeiten (zwei Threads - zwei Auswahlmöglichkeiten) und ein Münzwurf kann bestimmen, welchem ​​spezifischen Zustand gefolgt wird.


Danke, nette Erklärung. Und was ist mit Multi-Core-Maschine? Ich vermute, es gibt keine explizite Möglichkeit, Kerne in Java State Machine auszunutzen? Man muss sich dafür auf das Betriebssystem verlassen, oder?
Victor Sorokin

5
Eine Multi-Core-Maschine macht die Planung etwas komplexer. Da jedoch alle Kerne in einen gemeinsamen Speicher schreiben, bedeutet die Reihenfolge der Speicherschreibvorgänge zwischen den beiden Kernen, dass die Ausführung der Speicherschreibvorgänge im Wesentlichen verschachtelt ist. Das Betriebssystem nutzt Kerne und die JVM nutzt dies. Sie müssen nicht zweimal darüber nachdenken.
S.Lott

9

Das Schreiben von Sperrfunktionen richtet sich an Personen, die keine Zustandsautomaten erstellen können;)

Themen sind nützlich, wenn Sie nicht um das Blockieren herumkommen können. Keine grundlegende Computeraktivität blockiert wirklich, es ist nur so, dass viele von ihnen so implementiert sind, um die Bedienung zu vereinfachen. Anstatt ein Zeichen zurückzugeben oder "Lesen fehlgeschlagen", wird eine Lesefunktion blockiert, bis der gesamte Puffer gelesen ist. Anstatt in einer Warteschlange nach Rückkehrnachrichten zu suchen und zurückzukehren, wenn keine gefunden werden, wartet eine Verbindungsfunktion auf eine Antwort.

Sie können in einer Zustandsmaschine keine Sperrfunktionen verwenden (mindestens eine, die nicht "einfrieren" darf).

Und ja, die Verwendung von Zustandsautomaten ist eine praktikable Alternative. In Echtzeitsystemen ist dies die einzige Option, bei der das System ein Framework für die Maschine bereitstellt. Die Verwendung von Threads und Sperrfunktionen ist nur "der einfache Ausweg", da normalerweise ein Aufruf einer Sperrfunktion etwa 3-4 Zustände in der Zustandsmaschine ersetzt.


Ein Codepage-Fehler in einem Programm mit nur einem Ausführungskontext blockiert im Grunde genommen wirklich. Per Definition kann Code mit einem einzelnen Ausführungskontext erst dann einen Fortschritt erzielen, wenn der nächste Codeabschnitt im Ablauf verfügbar ist.
David Schwartz

1
@ David Schwartz: Stimmt, es ist grundsätzlich blockierend; aber es ist keine "Operation", da der blockierte Code dies nicht tut, sondern ihm passiert.
Javier

1
Das Lesen von Dateien blockiert nicht grundsätzlich - es kann immer in das Anfordern des Auslesens eines bestimmten Speicherorts und das Abrufen der Daten aus den Puffern unterteilt werden, nachdem die Anforderung erfüllt wurde. Und ein Seitenfehler ist eine Problemumgehung für die zufällige / heuristische Auslagerungsnutzung. Es tritt auf, wenn ein bestimmter Zustand eingegeben wurde, bevor alle für seine Ausführung erforderlichen Daten verfügbar waren - ein Mangel an Voraussicht, was dem Konzept der Zustandsmaschine widerspricht. Wenn Auslagerungs- und Einlagerungsvorgänge Teil des Zustandsautomaten sind, tritt kein Seitenfehler auf.
SF.

1
@David Schwartz: Die Definition des "Sperrverhaltens" unterliegt immer den "Echtzeit" -Anforderungen. Beispiel: Ein Codepage-Fehler gilt als nicht blockierend für Anwendungen, die eine Reaktionszeit in der Größenordnung von Hunderten von Millisekunden benötigen. Auf der anderen Seite würde eine Anwendung, die strenge Echtzeitanforderungen stellt, überhaupt keinen virtuellen Speicher belegen.
23.

1
@Mar: ... oder verwenden Sie einen deterministischen Swap-Algorithmus, der das Abrufen der erforderlichen Daten garantiert, bevor dies erforderlich wird.
SF.

9

Wie erreicht man Multithreading-Funktionalität in Hochsprachen wie Java mit nur einem Thread und einer Zustandsmaschine? Was ist zum Beispiel, wenn zwei Aktivitäten ausgeführt werden müssen (Berechnungen ausführen und E / A ausführen) und eine Aktivität blockieren kann?

Was Sie beschreiben, wird als kooperatives Multitasking bezeichnet , bei dem der CPU Aufgaben zugewiesen werden und diese nach einer selbstbestimmten Zeit oder Aktivität freiwillig freigegeben werden sollen. Eine Aufgabe, die nicht zusammenarbeitet, indem sie die CPU weiter nutzt oder das gesamte Werk blockiert und keinen Hardware-Watchdog-Timer hat, kann der Code, der die Aufgaben überwacht, nichts dagegen tun.

Was Sie in modernen Systemen sehen, wird als präemptives Multitasking bezeichnet. Dort müssen Aufgaben die CPU nicht freigeben, da der Supervisor dies für sie erledigt, wenn ein von der Hardware erzeugter Interrupt eintrifft. Die Interrupt-Serviceroutine im Supervisor speichert den Zustand der CPU und stellt ihn wieder her, wenn die Aufgabe das nächste Mal als zeitaufwändig eingestuft wird. Anschließend stellt sie den Zustand der nächsten auszuführenden Aufgabe wieder her und springt zurück, als wäre nichts geschehen . Diese Aktion wird als Kontextwechsel bezeichnet und kann teuer sein.

Ist die Verwendung von "state-machine only" eine sinnvolle Alternative zum Multithreading in Hochsprachen?

Lebensfähig? Sicher. Gesund? Manchmal. Ob Sie Threads oder eine Form von selbst gebrautem kooperativem Multitasking (z. B. Zustandsautomaten) verwenden, hängt von den Kompromissen ab, die Sie eingehen möchten.

Threads vereinfachen das Task-Design bis zu dem Punkt, an dem Sie jedes Programm als sein eigenes behandeln können, das zufällig den Datenraum mit anderen teilt. Dies gibt Ihnen die Freiheit, sich auf den jeweiligen Job zu konzentrieren, und nicht auf die gesamte Verwaltung und das gesamte Housekeeping, die erforderlich sind, damit der Job immer wieder ausgeführt wird. Da jedoch keine gute Tat ungestraft bleibt, zahlen Sie für all diese Bequemlichkeit bei Kontextwechseln. Viele Threads, die die CPU nach minimaler Arbeit auslasten (freiwillig oder durch blockierende Aktionen wie E / A), können beim Kontextwechsel viel Prozessorzeit in Anspruch nehmen. Dies gilt insbesondere dann, wenn Ihre Blockierungsvorgänge selten sehr lange blockieren.

Es gibt Situationen, in denen der kooperative Weg sinnvoller ist. Ich musste einmal eine Userland-Software für eine Hardware schreiben, die viele Datenkanäle über eine speicherabgebildete Schnittstelle übertrug, für die ein Abruf erforderlich war. Jeder Kanal war ein Objekt, das so aufgebaut war, dass ich es entweder als Thread ausführen oder wiederholt einen einzelnen Abfragezyklus ausführen konnte.

Die Leistung der Multithread-Version war aus genau dem Grund, den ich oben dargelegt habe, überhaupt nicht gut: Jeder Thread erledigte nur minimale Arbeit und gab dann die CPU frei, sodass die anderen Kanäle etwas Zeit haben konnten und viele Kontextwechsel verursachten. Das Freigeben der Threads bis zur Freigabe half beim Durchsatz, führte jedoch dazu, dass einige Kanäle nicht gewartet wurden, bevor die Hardware einen Pufferüberlauf erlebte, da sie nicht schnell genug eine Zeitscheibe erhielten.

Die Single-Threaded-Version, die sogar Iterationen jedes Kanals ausführte, lief wie ein verbrühter Affe, und die Last auf dem System fiel wie ein Stein. Die Strafe, die ich für die zusätzliche Leistung gezahlt habe, bestand darin, die Aufgaben selbst zu jonglieren. In diesem Fall war der Code, der dafür benötigt wurde, so einfach, dass die Kosten für die Entwicklung und Wartung die Leistungsverbesserung durchaus wert waren. Ich denke, das ist wirklich das Endergebnis. Wären meine Threads solche gewesen, die darauf gewartet hätten, dass ein Systemaufruf zurückkommt, hätte sich die Übung wahrscheinlich nicht gelohnt.

Das bringt mich zu Cox 'Kommentar: Threads sind nicht ausschließlich für Leute gedacht, die keine Zustandsautomaten schreiben können. Einige Leute sind durchaus in der Lage, dies zu tun, entscheiden sich jedoch für die Verwendung einer Dosen-Zustandsmaschine (dh eines Threads), um die Arbeit schneller oder mit geringerer Komplexität zu erledigen.


2

Was ist, wenn zwei Aktivitäten ausgeführt werden müssen (Berechnungen ausführen und E / A ausführen) und eine Aktivität blockieren kann?

Nun, ich kann mir ehrlich gesagt nicht vorstellen, wie ich mit blockierenden E / A ohne Threads umgehen soll. Es heißt Blocking Afterall, nur weil Code, der es aufruft, es muss wait.

Laut meiner Lektüre der Original-E-Mail von Cox (siehe unten) weist er darauf hin, dass das Threading nicht gut skaliert. Ich meine, was ist, wenn es 100 E / A-Anforderungen gibt? 1000? 10000? Cox weist darauf hin, dass eine große Anzahl von Threads zu schwerwiegenden Problemen führen kann:

Von: Alan Cox (alan@lxorguk.ukuu.org.uk)
Datum: Fr 21 Jan 2000 - 13:33:52 EST

Wenn Ihre Anwendung von einer großen Anzahl von Threads abhängt, treffen Sie immer wieder auf den Scheduler? Viele Leute werfen viele Threads auf ein Problem und es kann wirklich schlechtes Design sein.

Das ist die geringste Sorge. 1000 Threads sind 8 MB Kernel-Stacks und genug Tasks, um sicherzugehen, dass Sie den größten Teil Ihres Caches auch ausschalten können. Ein Computer ist eine Zustandsmaschine. Threads sind für Leute, die keine Zustandsautomaten programmieren können.

Es gibt viele Fälle, in denen Linux die Situation definitiv nicht verbessert, insbesondere asynchrone Block-E / A.

Alan

Quelle: Re: Interessante Analyse von Linux-Kernel-Threading durch IBM (Linux-Kernel-Mailinglisten-Archive)


1
  • Theoretisch stimmt das. Im wirklichen Leben sind Threads nur eine effiziente Abstraktion, mit der eine solche Zustandsmaschine programmiert wird. Sie sind so effizient, dass sie auch zum Programmieren von Zustandsdiagrammen und Petri-Netzen verwendet werden können (dh parallele Verhaltensweisen, bei denen Zustandsautomaten im Grunde sequentiell sind).

  • Das Problem bei Zustandsautomaten ist die kombinatorische Explosion. Die Anzahl der Zustände eines Computers mit 4G RAM beträgt 2 ^ (2 ^ 32) Zustände (ohne 2T-Plattenlaufwerk).

  • Für einen Mann, dessen einziges Werkzeug ein Hammer ist, sieht jedes Problem aus wie ein Nagel.


1

Threads sind die einzige Option in zwei Fällen:

  • mehrere Kerne ohne Speicheraufteilung zu verwenden.
  • mit externem Code umgehen, der blockiert.

Der zweite Grund ist, warum die meisten Leute denken, dass Threads für die IO- oder Netzwerkprogrammierung unvermeidbar sind, aber dies liegt normalerweise daran, dass sie nicht wissen, dass ihr Betriebssystem eine fortgeschrittenere API hat (oder nicht damit kämpfen möchten).

Aus Gründen der Benutzerfreundlichkeit und Lesbarkeit gibt es immer Ereignisschleifen (wie libev oder EventMachine ), die das Programmieren einer Zustandsmaschine fast so einfach machen wie das Ausführen von Threads, aber genügend Kontrolle bieten, um Synchronisierungsprobleme zu vergessen.


1
Das Beste aus zwei Welten: Kleine, blockierende Zustandsautomaten in Threads ermöglichen einen sehr einfachen Anwendungscode. Und die Fäden teilen sich schön in Kerne auf, wenn Sie sie haben. Wenn nicht alles mindestens zwei Kerne hat, wird es bald. dh Quad-Core-Arm-basierte Handys im Jahr 2012.
Tim Williscroft

0

Ein guter Weg, um die Interaktion zwischen Zustandsautomaten und Multithreading zu verstehen, ist der Blick auf GUI-Ereignishandler. Viele GUI-Anwendungen / Frameworks verwenden einen einzelnen GUI-Thread, der die möglichen Eingabequellen abfragt und für jede empfangene Eingabe eine Funktion aufruft. Im Wesentlichen könnte dies als ein riesiger Schalter geschrieben werden:

while (true) {
    switch (event) {
        case ButtonPressed:
        ...
        case MachineIsBurning:
        ....
    }
}

Jetzt wird schnell klar, dass die Kontrolle auf hoher Ebene in diesem Konstrukt nicht hoch sein kann: Der Handler für ButtonPressed muss ohne Benutzerinteraktion beendet werden und zur Hauptschleife zurückkehren, da sonst keine weiteren Benutzerereignisse stattfinden verarbeitet werden kann. Wenn ein Status gespeichert werden soll, muss dieser Status in globalen oder statischen Variablen gespeichert werden, jedoch nicht im Stapel. Das heißt, der normale Kontrollfluss in einer imperativen Sprache ist begrenzt. Sie sind im Wesentlichen auf einen Zustandsautomaten beschränkt.

Dies kann ziemlich chaotisch werden, wenn Sie verschachtelte Unterprogramme haben, die beispielsweise eine Rekursionsstufe speichern müssen. Oder Sie sind gerade dabei, eine Datei zu lesen, aber die Datei ist momentan nicht verfügbar. Oder sind nur in einer langen Berechnung. In all diesen Fällen ist es wünschenswert, den Status der aktuellen Ausführung zu speichern und zur Hauptschleife zurückzukehren, und dies ist Multithreading . Nicht mehr, nicht weniger.

Das Ganze wurde mit der Einführung von präemptivem Multithreading (dh dem Betriebssystem, das entscheidet, wann Threads die Kontrolle übernehmen sollen) etwas komplizierter, weshalb die Verbindung heute nicht sofort klar ist.

Um die letzte Frage zu beantworten: Ja, die Zustandsmaschine ist eine Alternative. Die meisten GUIs arbeiten auf diese Weise mit dem GUI-Thread. Schieben Sie die Zustandsmaschine einfach nicht zu weit, sie wird sehr schnell unerreichbar.


0

Die Frage, ob die Verwendung einer Zustandsmaschine in einer höheren Sprache sinnvoll ist, entspricht in etwa der Frage, ob das Schreiben in Assembler eine sinnvolle Alternative zur Verwendung einer höheren Sprache darstellt. Sie haben beide ihren Platz, wenn die richtige Situation gegeben ist.

Die Abstraktion der Verwendung von Threading erleichtert die Implementierung komplexerer Parallelsysteme, letztendlich haben jedoch alle Parallelsysteme die gleichen Probleme. Klassische Probleme wie Deadlock / Livelock und Prioritätsumkehr sind bei Systemen mit Zustandsautomaten genauso möglich wie bei Systemen mit gemeinsam genutztem Speicher parallel , NUMA oder sogar CSP , wenn sie komplex genug sind.


0

Ich glaube nicht, dass Zustandsautomaten ein sehr „elegantes“ Computerkonzept sind, aber wie Sie sagen, sie sind ziemlich kompliziert. Und komplizierte Dinge sind schwer in Ordnung zu bringen. Und Dinge, die nicht stimmen, sind einfach kaputt. Wenn Sie also nicht ein Genie von Alan Cox 'mutmaßlicher Statur sind, bleiben Sie bei dem, was Sie wissen, dass es funktioniert.

Sie können feststellen, wann jemand den vergeblichen Versuch unternommen hat, dies zu tun, da Sie (vorausgesetzt, es funktioniert gut) feststellen, dass die Aufgabe nahezu unmöglich ist, wenn es darum geht, sie zu warten. Das ursprüngliche "Genie" wird Sie mit dem Klumpen von schwer verständlichem Code zurücklassen (da diese Art von Entwicklern nicht allzu viele Kommentare hinterlassen, geschweige denn technische Dokumentationen).

In einigen Fällen ist eine Zustandsmaschine die bessere Wahl. Ich denke jetzt an eingebettete Typen, in denen einige Zustandsmaschinenmuster verwendet werden und die wiederholt und formalisiert verwendet werden (dh ordnungsgemäßes Engineering :)).

Es kann schwierig sein, das Threading richtig zu machen, aber es gibt Muster, die Sie dabei unterstützen - vor allem, wenn Sie weniger Daten zwischen den Threads austauschen müssen.

Der letzte Punkt dabei ist, dass moderne Computer sowieso auf vielen Kernen laufen, sodass eine Zustandsmaschine die verfügbaren Ressourcen nicht wirklich ausnutzt. Threading kann hier einen besseren Job machen.


2
Zustandsautomaten sind überhaupt nicht kompliziert! Komplexe Zustandsautomaten sind kompliziert, aber auch alle komplexen Systeme: o)
MaR

2
-1 für "nicht versuchen". Das ist der schlechteste Rat, den Sie geben können.
Javier

1
-1 "Versuch es nicht"? Das ist einfach dumm. Ich würde auch Ihre Behauptung anfechten, dass Zustandsmaschinen hart sind. Sobald Sie in so etwas wie eine heirarchalische Fuzzy-State-Machine geraten sind, ist es etwas komplizierter. Aber eine einfache Zustandsmaschine? Das sind ziemlich grundlegende Dinge, die ich jedes zweite Jahr in der Schule gelernt habe.
Steven Evers

Lassen Sie mich das "Versuchen Sie es nicht"
-Bit umformulieren

0

Gutes Beispiel für eine ordnungsgemäße Verwendung von Zustandsmaschinen anstelle von Threads: nginx vs apache2. Im Allgemeinen können Sie davon ausgehen, dass Nginx alle Verbindungen in einem Thread verarbeitet. Apache2 erstellt einen Thread pro Verbindung.

Für mich ist die Verwendung von Zustandsautomaten und Threads mit der Verwendung von perfekt handgefertigtem asm vs. viele andere Programmierer. Wenn Sie also derjenige sind, der einen schnellen Webserver erstellen möchte, verwenden Sie Statusmaschinen und asynchrone E / A. Wenn Sie das Projekt schreiben (nicht die Bibliothek, die überall verwendet werden soll), verwenden Sie Threads.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.