C# Multithreading

second.name

Lieutenant
🎅Rätsel-Elite ’24
Registriert
Sep. 2009
Beiträge
713
Hallo Community,
ich belese mich jetzt schon eine Weile über Multithreading in C#, allerdings verstehe ich noch immer nicht, wie man konkret eine Berechnung auf mehrere Threads aufteilt.

Code:
        static void Main(string[] args)
        {
            double x; double y;
            double InCycle = 1; double OutCycle = 1;
            double pi = 0;
            int diameter = 100;

            for (double i = 1; i < 10000000000; i++)
            {
                x = RandomNumberBetween(-diameter, diameter);
                y = RandomNumberBetween(-diameter, diameter);

                if (Math.Sqrt(Math.Pow(x, 2) + Math.Pow(y, 2)) > diameter)
                {
                    //InCycle++;
                }
                else
                {
                    OutCycle++;
                }

                pi = (4 * OutCycle / i);
            }

            Console.WriteLine(pi.ToString());
            Console.ReadLine();
        }

In diesem Beispiel wird Pi nach "Monte-Carlo" berechnet. Wie aber kann ich diese Berechnung "splitten" und wieder "zusammenführen"?

Danke und Gruß
 
Hallo,
du hast 3 Möglichkeiten:

Du kannst von der Klasse Thread ableiten, den BackgroundWorker verwenden und einfach die Tasks (TPL) verwenden.

Einfach mal nach diesen Begriffen mit der Suchmaschine deiner Wahl suchen. Da wirst du mit Beispielen und Informationen geradezu erschlagen...

greetz
hroessler
 
-- PI = ... muß aus der Schleife raus.

Wenn das passiert ist, kannst Du prinzipiell den Schleifenkörper in eine Funktion auslagern und mußt dieser dann natürlich Ein- und Ausgabeparameter mitgeben. Das funktioniert dann, wenn (wie in diesem Fall) die Schleifeniterationen voneinander unabhängig sind.


Diese Funktion könnte eine Signatur haben wie getInCircleMT(int diameter, ref isInCircle).

Als nächstes muß der Schreibzugriff auf isInCircle gekapselt werden. isInCircle ist eine Referenz auf den Zähler; jeder Thread kann theoretisch zur selben Zeit draufschreiben: Schlecht.

Als nächstes startet man die Schleife und in der Schleife startet man einen neuen Thread mit der eben festgelegten Funktion.

Wenn das passiert ist, laufen maximal so viele Threads, wie in der Schleifenobergrenze angegeben, gleichzeitig. Note: Es können nicht mehr sein, wohl aber weniger. Startet nämlich Thread #1000, dann kann Thread #1 schon fertig sein. Vielleicht aber auch nicht. Ab diesem Punkt wird es eher undurchsichtig.

Nach dem Ende der Schleife weiß man: die erforderlichen Berechnungen sind gestartet. Man weiß nicht, ob sie auch beendet sind.

Deswegen prüft man als nächstes -bzw hat einen Eventhandler- um zu erfahren, ob noch Threads aktiv sind. Threads noch aktiv heißt, wir haben noch nicht die gesamte Information. Hierfür also entweder eine Schleife mit sleep() oder einen Eventhandler bauen und jeden Thread ein Event abschicken lassen, wenn er fertig ist. (Ggf verschiedene Events wie "Fertig, aber Fehler"; dann kann man normalerweise endgültig abbrechen).

Jetzt sind alle Threads fertig. Wir können also die Variable isInCircle wieder anfassen, welche ganz der obigen Logik folgend die Anzahl der innerhalb des Kreises gelegenen Punkte hochgezählt hatte.

Was bleibt, ist die Ausgabe von Pi = 4*isInCircle / 10000000000 (nicht i, das ist ein Fehler oben im Quellcode).

Zusätzlich kann man sich noch seinen Threadpool konfigurieren, wo man dann einstellen kann, wieviele Threads gleichzeitig laufen dürfen und was passieren soll, wenn ein Thread fertig ist und andere noch gewartet haben.


"Theoretisch" könnte man die obige Berechnung auf 10000000000 CPU-Kernen ausführen. Hat man weniger, sollte man auch eine Threadobergrenze festlegen. Der Threadpool (nicht die Anwendung) kümmert sich dann um die Verwaltung der Threads.
 
Danke vielmals für die ausführlichen Antworten: :)

Das wird mein Abendprogramm und bei Fragen melde ich mich nochmal. ;)
 
Verwende am besten die Parallel.For() Methode aus der TPL.
Für so kurze Berechnungen auf keinen Fall selber Threads erzeugen. Das wäre bei so kurzen Codeblöcken um ein vielfaches langsamer. Die TPL nutzt die bereits vorhandenen Threads im Threadpool ohne den ganzen Overhead, den das Erzeugen von Threads mit sich bringt.

Hier ist ein etwas übertrieben großes Beispiel von Microsoft. Das Prinzip sollte damit aber klar werden:
https://msdn.microsoft.com/de-de/library/dd460713(v=vs.110).aspx

Edit: Habe gerade einen kleinen Code als Beispiel geschrieben wie ich es für ideal halte. Weil ich Dir den Spaß nicht nehmen möchte packe ich das mal in einen Spoiler:

Code:
using System;
using System.Threading;
using System.Threading.Tasks;

namespace CsTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var iterations = 100000000L;
            var inside = 0L;
            var cpuCores = Environment.ProcessorCount;
            var iterationsPerCore = iterations / cpuCores;

            Parallel.For(0, cpuCores, core =>
            {
                var rnd = new Random(core);

                for (long i = 0; i < iterationsPerCore; ++i)
                {
                    var x = rnd.NextDouble();
                    var y = rnd.NextDouble();

                    if (Math.Sqrt(x * x + y * y) <= 1.0)
                        Interlocked.Increment(ref inside);
                }
            });

            Console.WriteLine("PI = {0}", 4 * ((double)inside / iterations));
            Console.ReadKey();
        }
    }
}

Anmerkungen zu Deinem Code:
Den Datentyp der Variable i in würde ich auf long ändern. Operationen mit Gleitkommazahlen dauern länger.
x*x ist wahrscheinlich schneller als Math.Pow(x,2).
pi innerhalb der Schleife zu aktualisieren macht wenig Sinn.
Kommt bei Dir der richtige Wert raus? Eigentlich müsstest Du InCycle für die Berechnung nehmen.
 
Zuletzt bearbeitet:
Klar, so ist es schön aufgeräumt und nicht minder klar performanter. :)

Ich hatte mich eher an dem "ich hab's noch nicht verstanden" aufgehängt. "Wie teile ich das auf? Wie krieg ich es wieder zusammen?"

War möglicherweise ein Mißverständnis. Wichtig ist halt -imo-, daß klar ist, wie man von Quelle S zu Ziel D kommt, wenn der Weg dazwischen parallelisiert sein soll.

Ansonsten bau ich irgendwas und am Ende kommt irgendwas(tm) raus und ich weiß nicht warum.

Ist das Verständnis natürlich da, kann man das ganz anders angehen, das ist richtig.
 
@RalphS
Stimmt, das "wie" habe ich bei meiner Antwort unter dem Teppich gekehrt. Deine Antwort hat es schon im Ansatz richtg erklärt. Funktion auslagern und dann von mehreren Threads oder Tasks aufrufen und die Statusvariablen vor parallelem Zugriff schützen. Vom Prinzip her eigentlich ganz einfach. Der Teufel steckt im Detail. Alleine schon die Vielfalt an Locking-Mechanismen für Variablen ist verwirrend, siehe hier.
 
Zuletzt bearbeitet:
Wie schon gesagt Multithreading verursacht auch Overhead, das bringt nur was wenn die einzelnen Threads auch genug zu tun haben. Zum üben ist das natürlich erstmal egal, aber in echten Anwendungen muss man wirklich schauen wo das was bringt Bzw wie man das implementiert damit es was bringt.
 
Wenn es nix zu parallelisieren gibt, gibt es nix zu parallelisieren. :)

"Ideal" ist natürlich, wenn man die Ergebnisse der Teilaufgaben nicht zur Weiterverarbeitung braucht. Einfach Threads loslaufen lassen und irgendwann sind die halt fertig.... und fertig. Dann noch ne kurze Routine basteln, die aufs Fensterschließen reagiert und die evtl noch laufenden Threads entweder abwartet oder halt beendet.

"Anzahl Cores" an Archivdateien auspacken, wenn die auf einer SSD liegen und auch auf eine geschrieben werden zum Bleistift.

Dinge, die im Hintergrund ausgeführt werden können. Mail schreiben => abschicken. Warum soll der Benutzer warten, bis die Mail wirklich raus ist? Ab in einen extra Thread damit.

Aber wenn man eh warten muß, daß die Teilaufgabe fertig ist, dann muß das auch nicht in einen extra Thread ausgelagert werden. Auf rot folgen drei Sekunden Gelb und dann kommt Grün - da gibt's nix zu parallelisieren, das passiert hübsch in Abhängigkeit voneinander in eben dieser Reihenfolge.

Und eine Schleife, die -sinngemäß- ungerade Zahlen bis n addiert, naja die könnte man sicherlich parallelisieren, aber wenn man außer von Programmierung auch noch ein bißchen Ahnung von Mathematik hatte... spart man sich außer der Parallelisierung auch noch die Schleife. ;)
 
"Anzahl Cores" an Archivdateien auspacken, wenn die auf einer SSD liegen und auch auf eine geschrieben werden zum Bleistift.

Dinge, die im Hintergrund ausgeführt werden können. Mail schreiben => abschicken. Warum soll der Benutzer warten, bis die Mail wirklich raus ist? Ab in einen extra Thread damit.
Bei I/O Geschichten mit Festplatten oder Sockets setzt man eher auf asynchrone Programmierung. Damit kann man sich Threads sparen. Da hat C# das tolle async/await Interface. Im Prinzip wird da die Aufgabe an das I/O Gerät weitergegeben. Das meldet sich per Interrupt wenn es fertig ist und löst damit eine Aktion im Code aus. Ergo braucht man keinen Thread, der auf das Ende wartet.
 
Danke für den Hinweis und den Link. :)

Ich geb gerne zu, daß ich zuviel zum Selberimplementieren neige, was -wie hier- nicht immer der beste Weg ist.

Ohne jetzt nutzlose Diskussionen vom Zaun brechen zu wollen (ich gehe davon aus, daß es deswegen den Link gab): am Ende ist es immer noch in 'irgendeiner' Form parallelisierte Verarbeitung, egal wie die jetzt real implementiert sein mag. So gesehen befinden sich bereits Events in einer separaten Ausführungsschicht - würden sie synchron ausgeführt, wären sie nutzlos -- Threads gibt's ja dafür (meines Wissens) auch nicht.

Zentral ist halt für asynchrone Verarbeitung immer, daß man sich das überlegen muß, was man wo anfangen kann und was bis wo beendet sein muß. Dies im Gegensatz zu synchroner Verarbeitung, wo man bei Schritt 2 sicher weiß, daß das Ergebnis entweder da ist oder etwas schiefgelaufen war.
"Wie" das umgesetzt war... das folgt erst darauf. Gibt X mögliche Wege, Aufgabe 2 asynchron bzgl Aufgabe 1 auszuführen, und wie Du ja schreibst, ist nicht jeder Weg für jedes Problem die beste Lösung.

Mir persönlich reicht an der Stelle erstmal, daß es überhaupt funktioniert. Aber ich muß das ja auch nicht irgendwo abliefern, sondern blamiere mich bestenfalls vor mir selber und hier im Forum. :lol:
 
Zurück
Oben