Migrieren Sie eine App mit einem Thread auf eine Multi-Threaded-Parallel-Carlo-Simulation

8

Ich wurde damit beauftragt, eine existierende Single-Thread-Monte-Carlo-Simulation zu übernehmen und zu optimieren . Dies ist ac # console app, kein db Zugriff es lädt Daten einmal aus einer CSV-Datei und schreibt es am Ende, so ist es ziemlich genau CPU gebunden , auch nur etwa 50 MB Speicher verwendet.

Ich habe es durch Jetbrains dotTrace Profiler ausgeführt. Von der gesamten Ausführungszeit erzeugen etwa 30% einheitliche Zufallszahlen, 24% übersetzen gleichförmige Zufallszahlen in normalverteilte Zufallszahlen.

Der grundlegende -Algorithmus besteht aus einer ganzen Menge verschachtelter for-Schleifen mit Zufallszahlenaufrufen und Matrixmultiplikation in der Mitte, jede Iteration gibt ein Double zurück, das zu einer Ergebnisliste hinzugefügt wird sortiert und getestet für einige Konvergenzkriterien (bei Prüfpunkten alle 5% der gesamten Iterationszählung), wenn das Programm aus den Schleifen heraus bricht und die Ergebnisse schreibt, sonst geht es bis zum Ende.

Ich möchte, dass die Entwickler sich darauf einlassen:

  • sollte ich new Thread v ThreadPool verwenden
  • sollte ich mir die Microsoft Parallels Extension-Bibliothek ansehen
  • sollte ich mir AForge.Net Parallel.Für , Ссылка weitere Bibliotheken ansehen ?

Einige Links zu Tutorials zu den oben genannten Informationen sind sehr willkommen, da ich noch nie einen parallelen oder mehrsprachigen Code geschrieben habe . .

  • beste Strategien zur Erzeugung von in der Masse verteilten normalverteilten Zufallszahlen und deren anschließende Verwendung. Einheitliche Zufallszahlen werden in diesem Zustand nie von der App verwendet, sie werden immer in normalverteilt übersetzt und dann verbraucht.
  • gute schnelle Bibliotheken (parallel?) zur Zufallsgenerierung
  • Speicher Überlegungen, wie ich diese Parallele nehmen , wie viel extra werde ich benötigen.

Die aktuelle App benötigt 2 Stunden für 500.000 Iterationen. Das Unternehmen muss diese auf 3.000.000 Iterationen skalieren und mehrere Male am Tag aufgerufen werden, was eine starke Optimierung erfordert.

Besonders möchten Sie von Leuten hören, die Microsoft Parallels Extension oder AForge.Net Parallel

verwendet haben

Dies muss relativ schnell produktiviert werden, also .net 4 beta ist out obwohl ich weiß, dass es Concurrency-Bibliotheken eingebaut hat, können wir später auf die .net 4-Migration schauen freigegeben. Im Moment hat der Server .Net 2, ich habe zur Überprüfung ein Upgrade auf .net 3.5 SP1 eingereicht, das meine Dev-Box hat.

Danke

Aktualisieren

Ich habe gerade die parallele.Für die Implementierung versucht, aber es kommt mit einigen seltsamen Ergebnissen. Einfaches Gewinde:

%Vor%

An:

%Vor%

Innerhalb simulieren gibt es viele Aufrufe an rnd.nextUniform (), Ich denke, ich bekomme viele Werte, die die gleichen sind , wird dies wahrscheinlich passieren, weil dies jetzt parallel ist?

Vielleicht sind auch Probleme mit dem List-AddRange-Aufruf nicht threadsicher? Ich sehe das

System.Threading.Collections.BlockingCollection ist vielleicht sinnvoll, aber es hat nur eine Add-Methode no AddRange, also müsste ich dort Ergebnisse suchen und in einer Thread-sicheren Weise hinzufügen. Jeder Einblick von jemandem, der Parallel verwendet hat. Für sehr geschätzt. Ich wechselte für meine Aufrufe vorübergehend zu System.Random , da ich beim Aufruf von nextUniform mit meiner Mersenne Twister-Implementierung eine Ausnahme erhielt, vielleicht war es nicht threadsicher ein bestimmtes Array bekam einen Index außerhalb der Grenzen ....

    
m3ntat 12.07.2009, 18:24
quelle

3 Antworten

13

Zuerst müssen Sie verstehen, warum Sie denken, dass die Verwendung mehrerer Threads eine Optimierung ist - wenn dies nicht der Fall ist. Wenn Sie mehrere Threads verwenden, wird Ihre Arbeitslast schneller only , wenn Sie mehrere Prozessoren haben, und dann maximal so viel schneller, wie Sie über CPUs verfügen (dies wird Beschleunigung ). Die Arbeit ist nicht im traditionellen Sinne des Wortes "optimiert" (d. H. Die Menge an Arbeit wird nicht reduziert - tatsächlich wächst bei Multithreading die Gesamtmenge an Arbeit typischerweise aufgrund des Threading-Overheads).

Sie müssen also beim Entwerfen Ihrer Anwendung Arbeiten finden, die parallel oder überlappend ausgeführt werden können. Es kann möglich sein, Zufallszahlen parallel zu erzeugen (indem mehrere Zufallszahlen auf verschiedenen CPUs laufen), aber das würde auch die Ergebnisse verändern, wenn Sie verschiedene Zufallszahlen erhalten. Eine weitere Option ist die Generierung der Zufallszahlen auf einer CPU und alles andere auf verschiedenen CPUs. Dies kann zu einer maximalen Beschleunigung von 3 führen, da der RNG weiterhin sequenziell ausgeführt wird und immer noch 30% der Last aufnimmt.

Wenn Sie also diese Parallelisierung durchführen, erhalten Sie 3 Threads: Thread 1 führt den RNG aus, Thread 2 erzeugt die Normalverteilung und Thread 3 erledigt den Rest der Simulation.

Für diese Architektur ist eine Producer-Consumer-Architektur am besten geeignet. Jeder Thread liest seine Eingabe aus einer Warteschlange und erzeugt seine Ausgabe in einer anderen Warteschlange. Jede Warteschlange sollte blockiert werden. Wenn der RNG-Thread zurückfällt, wird der Normalisierungs-Thread automatisch blockiert, bis neue Zufallszahlen verfügbar sind. Aus Effizienzgründen würde ich die Zufallszahlen in einem Array von beispielsweise 100 (oder größer) Threads übergeben, um Synchronisationen für jede beliebige Zufallszahl zu vermeiden.

Für diesen Ansatz benötigen Sie kein erweitertes Threading. Verwenden Sie nur reguläre Thread-Klasse, keinen Pool, keine Bibliothek. Das einzige, was Sie brauchen, ist (leider) nicht in der Standardbibliothek eine blockierende Queue-Klasse (die Queue-Klasse in System.Collections ist nicht gut). Codeproject bietet eine vernünftig aussehende Implementierung von einem; da sind wahrscheinlich andere.

    
Martin v. Löwis 12.07.2009, 18:48
quelle
1

List<double> ist definitiv nicht Thread-sicher. Weitere Informationen finden Sie im Abschnitt "Thread-Sicherheit" in der System.Collections.Generic.List-Dokumentation . Der Grund ist Leistung: Das Hinzufügen von Thread-Sicherheit ist nicht kostenlos.

Ihre Zufallszahlenimplementierung ist auch nicht Thread-sicher; die gleichen Zahlen mehrmals zu erhalten, ist genau das, was Sie in diesem Fall erwarten würden. Lassen Sie uns das folgende vereinfachte Modell von rnd.NextUniform() verwenden, um zu verstehen, was passiert:

  1. berechnen Pseudo-Zufallszahl aus der aktuelle Status des Objekts
  2. Aktualisiere den Zustand des Objekts, so dass der Der nächste Anruf führt zu einer anderen Nummer
  3. gib die Pseudozufallszahl zurück

Wenn nun zwei Threads diese Methode parallel ausführen, kann so etwas passieren:

  • Thread A berechnet eine Zufallszahl wie in Schritt 1.
  • Thread B berechnet eine Zufallszahl wie in Schritt 1. Thread A hat noch nicht aktualisiert den Zustand des Objekts, so Das Ergebnis ist das gleiche.
  • Thread A aktualisiert den Status von Objekt wie in Schritt 2.
  • Thread B aktualisiert den Status von Objekt wie in Schritt 2, Trampling über A-Status Änderungen oder vielleicht das gleiche geben Ergebnis.

Wie Sie sehen, ist jede Argumentation, die Sie ausführen können, um zu beweisen, dass rnd.NextUniform() funktioniert, nicht mehr gültig, weil sich zwei Threads gegenseitig stören. Schlimmer noch, Bugs wie diese hängen vom Timing ab und erscheinen nur selten als "Pannen" unter bestimmten Arbeitslasten oder auf bestimmten Systemen. Albtraum debuggen!

Eine mögliche Lösung ist die Eliminierung der Statusfreigabe: Geben Sie jeder Aufgabe einen eigenen Zufallszahlengenerator , der mit einem anderen Startwert initialisiert wurde (vorausgesetzt, Instanzen teilen den Status nicht durch statische Felder in irgendeiner Weise).

Eine andere (minderwertige) Lösung besteht darin, ein Feld zu erstellen, das ein Sperrobjekt in Ihrer MersenneTwister -Klasse wie folgt enthält:

%Vor%

Verwenden Sie diese Sperre dann in Ihrer MersenneTwister.NextUniform() -Implementierung:

%Vor%

Dies verhindert, dass zwei Threads die NextUniform () -Methode parallel ausführen. Das Problem mit der Liste in Ihrem Parallel.For kann auf ähnliche Weise behoben werden: Trennen Sie den Aufruf Simulate und den Aufruf AddRange , und fügen Sie dann Sperren um den Aufruf AddRange hinzu.

Meine Empfehlung: Vermeide es, irgendeinen veränderlichen Zustand (wie den RNG-Zustand) zwischen parallelen Aufgaben zu teilen, wenn irgend möglich. Wenn kein veränderbarer Status freigegeben ist, treten keine Threading-Probleme auf. Dies vermeidet auch Engpässe bei der Sperrung: Sie möchten nicht, dass Ihre "parallelen" Tasks auf einen einzelnen Zufallszahlengenerator warten, der überhaupt nicht parallel arbeitet. Besonders wenn 30% der Zeit damit verbracht wird, Zufallszahlen zu sammeln.

Beschränken Sie die Freigabe und Sperrung von Zuständen auf Orte, an denen Sie sie nicht vermeiden können, z. B. beim Zusammenfassen der Ergebnisse der parallelen Ausführung (wie in Ihren AddRange -Aufrufen).

    
Wim Coenen 13.07.2009 10:56
quelle
0

Threading wird kompliziert sein. Sie müssen Ihr Programm in logische Einheiten aufteilen, die jeweils auf ihren eigenen Threads ausgeführt werden können, und Sie müssen mit allen auftretenden Problemen im Zusammenhang mit Parallelität fertig werden.

Die parallele Erweiterungsbibliothek sollte es Ihnen ermöglichen, Ihr Programm zu parallelisieren, indem Sie einige Ihrer for-Schleifen in Parallel.For Schleifen ändern. Wenn Sie sehen möchten, wie das funktioniert, geben Anders Hejlsberg und Joe Duffy in ihrem 30-minütigen Video hier eine gute Einführung:

Ссылка

Threading vs. ThreadPool

Der ThreadPool ist, wie der Name schon sagt, ein Pool von Threads. Die Verwendung des ThreadPools zum Erhalten Ihrer Threads hat einige Vorteile. Thread-Pooling ermöglicht es Ihnen, Threads effizienter zu verwenden, indem Sie Ihrer Anwendung einen Pool von Worker-Threads bereitstellen, die vom System verwaltet werden.

    
Robert Harvey 12.07.2009 18:48
quelle