ruwen hat geschrieben:Man kann in manchen Sprachen die Anzahl der Cores auslesen und dann dem entsprechend optimieren. In meinem jugendlichen Leichtsinn vermute ich mal, dass man eher allgemein auf mehrere Cores optimiert (8 Threads können auch von einem Core abgearbeitet werden, nur dass dann etwas Scheduling-Overhead entsteht). Und selbst wenn deine Anwendung nicht optimiert ist, überleg mal was normalerweise noch so alles nebenbei läuft. Schwachsinn würde ich es deshalb nicht nennen.
Die Anzahl der CPUs auslesen ist ja keine Sache, das kannst du mit ``grep -c ^processor < /proc/cpuinfo`` ganz trivial haben. Nichts sonderlich anderes ist eben Multicore: mehrere CPUs die auf einem Baustein sitzen und sich einige Hardware teilen. Jede dieser CPUs kann nun genau einen Thread laufen lassen, was aber recht selten genau so ausgelastet ist, da oft ein Kernel dazwischenhängt und ein Userland, das auch einige Threads hat. Somit braucht es einen Scheduler der Threads verteilt. Optimalerweise ist der Scheduler ausreichend schlau, dass da eine möglichst hohe Auslastung erreicht wird. Hier erreichen wir das erste Problem: Skalierbarkeit. Viele Betriebssysteme sind nicht sonderlich für große Anzahlen von CPUs gedacht, so dass das hinzufügen von weiteren CPUs nur wenig zur Performanceverbesserung beiträgt. Ich meine mich zu erinnern, dass bei
SMP-Betrieb, also genau das was Multicores auf x86 machen FreeBSD bis zu 8 CPUs davon profitiert und danach lohnt es sich nicht weitere Kerne/CPUs hinzuzufügen. Weiter geht es dann mit
NUMA.
Nun, jetzt nehmen wir an wir haben einen Dualcore, so wie ich in meinem Laptop. Habe hier nicht so wahnsinnig viel offen, also das wären dann laut ``ps -eLf | tail -n+2 | wc -l`` 101 Threads. Viele Applikationen nutzen nur einen Thread, einige nutzen mehrere. Die Anzahl der Threads besagt wie viele CPUs diese einzelne Applikation im Optimalfall ausreizen könnte. Bei 1 Thread kann die Applikation, sollte sie 100% Rechenzeit benötigen eine CPU komplett auslasten, also das gesamte System auf maximal 50%. Mit den anderen 50% kann ich dann machen was ich will. Ein intelligenter Scheduler schiebt dann den einen Thread auf eine CPU und die anderen, die nicht viel machen auf die andere CPU. Eine Applikation mit nur einem Thread zu machen ist ganz einfach: man nutzt einfach so viel Rechenleistung wie man braucht und das OS kümmert sich darum, so viel Rechenleistung wie im Moment beschaffbar ist, dem Thread zur Verfügung zu stellen.
Nun, jetzt nehmen wir uns eine weitere Applikation vor, etwa eine kleine GUI-Anwendung. Diese Anwendung berechnet irgendwelche komplexen Sachen im Hintergrund. Da die Berechnung den Thread so lange blokiert bis sie fertig würde würde es scheinen, dass die Anwendung hängt, weil sie auch nicht mehr dazu kommt ihr Fenster neu zu zeichnen und auf Usereingaben zu reagieren. Daher lagert man die Berechnung in einen separaten Thread aus. Auf einer Maschine mit 2 CPUs könnte nun der Scheduler entscheiden den Berechnungsthread auf die eine CPU zu tun und den GUI-Thread auf eine andere. Oder auch nicht, das ist ihm überlassen. Somit laufen die Threads eben real parallel. Zumindest theoretisch, da es ja noch die 101 Threads gibt, die zwischendrin auch ausgeführt werden wollen. Auf einer 1 CPU Maschine läuft das eben so ab, das dann einfach 103 Threads in irgendeiner Reihenfolge irgendwie lange bestimmte Zeitscheiben abbekommen und dann der nächste Thread läuft (Preemptive Multitasking nennt man das). Nun ist der Berechnungsthread aber immer noch nur ein einzelner Thread und kann damit nur eine CPU auslasten. Der andere Thread, der GUI-Thread verbraucht kaum Ressourcen. Somit wird die Anwendung durch Zugabe weiterer Prozessoren kaum messbar schneller.
Die Schwierigkeit besteht also darin die Berechnung auf mehrere Threads aufzuteilen. Das ist einfach wenn das Problem parallelisierbar ist (wie etwa Kompilation von C-Quelltext mit GCC, wo man einzelne Quelldateien parallel zueinandern zu Objektcode verarbeiten kann und dann erst das Ergebnis zusammenlinkt) aber schwerer wenn man für eine Berechnung die Ergebnisse einer anderen Berechnung braucht, wie das bei vielen Sachen der Fall ist, wie etwa die Größe der Mails in einem IMAP-Ordner zu bestimmen oder tausend anderen Problemen. Ein weiteres Problem ist Datenaustausch zwischen Threads. Mehrere Threads greifen auf den gleichen Speicher zu, aber wenn man zwei Threads startet, diese eine Speicherzelle auslesen und dann erstmal der eine in die Speicherzelle schreibt und dann der andere in die selbe Zelle schreibt gehen Ergebnisse verloren und Race-Conditions entstehen. Da nutzt man dann häufig Locking, wobei dann der Schreibzugriff durch bestimmte Flags zu bestimmten Zeiten blokiert wird. Dabei kann es dann auch vorkommen, dass bestimmte Locks nie aufgehoben werden und Deadlocks entstehen. Wenn man es richtig macht, und das Problem passt, kann man nahezu 100% Auslastung erreichen. Multithreading is hard, let's go shopping!
Um den Entwicklern zu helfen gibt es nun Bibliotheken die Dinge vereinfachen wie etwa OpenMP, Thread Building Blocks oder Boost.Thread und wo genau geschaut wurde dass die Dinge die sie bieten wirklich fehlerfrei sind und keine Race-Conditions oder Deadlocks haben. Parallel dazu gibt es einen Trend zu funktionalen Sprachen mit immutablen Datenstrukturen, also Datenstrukturen die sich nicht ändern können, statt alte zu ändern werden neue, geänderte Datenstrukturen frisch erstellt. Klingt aufwendig, ist es aber gar nicht mal so; durch einige intelligente Optimierungstricks kann man da halbwegs brauchbare Performance rausholen und für den Programmierer ist es einfacher korrekten Code zu schreiben. Solche Kandidaten wären etwa Erlang (ejabberd, der Jabber-Server des Debianforums ist in Erlang und kann wirklich viele user auf einmal handhaben), Scala wie es der Name schon impliziert oder Clojure. Die Sprachen gehen da Teils noch weiter und kümmern sich gar nicht um Threads sondern eher um Tasks, die dann irgendwie von der Runtime auf Threads abgebildet werden und dann von der Runtime parallelisiert werden.
Heutige Programme nutzen selten Threads dermaßen extensiv eben weil es komplexer ist und viele Probleme nicht trivial parallelisierbar sind. Momentan machen sich hauptsächlich die Autoren von rechenintensiven Programmen gedanken darüber, aber wenn etwa 12-Kernsysteme normal sein werden ist es natürlich nicht so toll wenn da zum beispiel ein Officeprogramm den Rechner nur maximal 1/12 auslasten kann wenn man genug Hardware hat um eine Berechnung in einer umfangreichen Tabellenkalkulation mal eben 12 mal schneller auszuführen. Fun fact: einige der an das NIST eingesendeten SHA Nachfolge-Kandidaten sind zwar rechenintensiver als SHA aber schneller, da die Autoren Algorithmen nutzen die besser parallelisierbar sind.
Wir wollten einen Marsch spielen, aber wir hatten nur Xylophone.