Laden...

Threads mit Delegate-Funktion in einer Schleife benutzen Zählvariable gleichzeitig

Erstellt von ByteDevil vor 7 Jahren Letzter Beitrag vor 7 Jahren 5.110 Views
ByteDevil Themenstarter:in
132 Beiträge seit 2013
vor 7 Jahren
Threads mit Delegate-Funktion in einer Schleife benutzen Zählvariable gleichzeitig

Hallo liebe Community,

ich hoffe ihr könnt mir auch diesmal wieder helfen...ich hab ein großes Verständnisproblem und war schon drauf und dran den Exorzisten wegen meines PC's zu rufen 😕

Ich habe ein Programm geschrieben das Fraktale generiert. Das wollte ich nun beschleunigen, indem ich die Berechnung auf mehrere Threads verteile. Dabei passiert etwas das ich mir beim besten Willen nicht erklären kann. Ich habe mal einen kurzen Beispielcode geschrieben, der den Fehler in wenigen Zeilen reproduziert. Auf dem Screenshot seht ihr wie er sich verhält. Hier auch nochmal für Copy & Paste:


using System;
using System.Threading;

namespace Multithread_test
{
    class Program
    {
        static int[,] matrix;
        static int runningThreads = 0;

        static void Main(string[] args)
        {
            matrix = new int[1000, 1000];

            for (int y = 0; y < 1000; y++)
            {
                Thread thread = new Thread(delegate () { CalcRow(y); });
                thread.Start();
                runningThreads++;
            }

            while (runningThreads > 0)
                Thread.Sleep(10);

            Console.WriteLine("Fertig!");
            Console.ReadKey();
        }

        static void CalcRow(int y)
        {
            Console.WriteLine("Bearbeite Zeile " + y);
            for (int x = 0; x < 1000; x++)
                matrix[x, y] = x * y;
            runningThreads--;
        }
    }
}

Ich probiere nun schon seit Stunden rum...wie kann es sein das mehrere Threads mit dem gleichen Parameter für y aufgerufen werden? Auch wird die Methode mit y-Werten über 999 aufgerufen, was doch laut der Abbruchbedingung der Schleife gar nicht sein kann!?
Einige Zeilen werden auch einfach übersprungen und gar nicht berechnet.

Bitte erleuchtet mich...

Viele Grüße,
ByteDevil

2.078 Beiträge seit 2012
vor 7 Jahren

Du hast eine gemeinsame y-Variable für jeden Thread hast.
Wenn Du den 1000ten Thread mit y=999 gestartet hast, dann dauert es einen kurzen Moment bis der Thread und damit der Delegat gestartet wird. In der Zeit hat die for-Schleife aber schon weiter gezählt und steht auf 1000. Der Code in der Schleife wird nicht ausgeführt, weil die Bedingung nicht erfüllt ist (kleiner als 1000), dennoch ist der Wert, der in dem letzten Thread verwendet wird, 1000.

Deshalb überspringt er auch y=0, denn der Thread, der y=0 bearbeiten soll, läuft erst an, wenn die for-Schleife schon weiter gezählt hat.

Es reicht schon, wenn Du den Wert in eine Variable zwischenspeicherst. Diese Zwischen-Variable wird von der for-Schleife nicht behandelt.
Besser wäre aber, wenn Du Parallel.For verwendest, das tut ungefähr genau das, was Du willst.

Beachte aber auch: Das Starten eines Threads kostet Zeit und das nicht wenig. Du erstellst für jeden Durchlauf einen Thread und jedes mal kostet das viel Zeit.
Parallel.For kann die Aufgaben auf wenige Threads aufteilen. Begrenzt Du auf vier Threads, dann führt jeder der vier Threads 250 Berechnungen durch, das ist viel effektiver.

ByteDevil Themenstarter:in
132 Beiträge seit 2013
vor 7 Jahren

Wie meinst du das sie teilen sich die y-Variable? Die in CalcRow und der Main-Methode haben doch nichts miteinander zu tun. Auch wenn ich y in CalcRow umbenenne, ändert das nichts an dem Verhalten.

Wie kann ich denn Parallel.For auf eine methode mit Parameter anwenden?

2.078 Beiträge seit 2012
vor 7 Jahren

Du hast aber noch einen Delegaten dazwischen, erst darin wird deine Methode aufgerufen und darin wird auch die y-Variable verwendet. Und dieser Delegat wird aufgerufen, nachdem die for-Schleife weiter gezählt hat.

ByteDevil Themenstarter:in
132 Beiträge seit 2013
vor 7 Jahren

Danke Palladin007 😃

Ich habe deinen Rat beherzigt und einfach zu beginn der Schleife den Schleifenzähler zwischengespeichert. Nun funktioniert es perfekt und die Berechnung läuft fast 10 mal so schnell wie mit nur einem Thread 😃

Liebe Grüße,
ByteDevil

S
322 Beiträge seit 2007
vor 7 Jahren

Hallo,

hier noch ein Beispiel mit Parallel.For:


internal class Program
    {
        private static int[,] matrix;

        private static void Main(string[] args)
        {
            Stopwatch watch = Stopwatch.StartNew();
            matrix = new int[1000, 1000];

            Parallel.For(0, 1000, y =>
            {
                CalcRow(y);
            });

            Console.WriteLine("Fertig! nach " + watch.ElapsedMilliseconds.ToString("0 ms"));
            Console.ReadKey();
        }
        
        private static void CalcRow(int y)
        {
            Console.WriteLine("Bearbeite Zeile " + y);
            for (int x = 0; x < 1000; x++)
                matrix[x, y] = x * y;
        }
    }

C
2.121 Beiträge seit 2010
vor 7 Jahren

war schon drauf und dran den Exorzisten wegen meines PC's zu rufen

Verständlich 😃

An sowas bin ich auch schon hängen geblieben. Das ist ordentlich böse. Mir hat es geholfen sich das so vorzustellen.
Du sagst jedem Thread, wenn du startest nutze den Wert y.
Jetzt läuft deine Schleife durch und legt die Threads an. Wann der Thread losläuft ist nicht bestimmbar, jedenfalls nicht direkt sofort. Erst mal darf die Schleife noch weiterlaufen. Wann der Thread dann in y nachsieht ist auch wieder unklar.
Im Extremfall läuft erst deine Schleife komplett durch und erst dann werten die Threads y aus. Dann sehen alle Threads den selben Wert.
In der Realität wird es so sein dass die Schleife ein paar Werte hochzählt, dann kommen ein paar wartende Threads an die Reihe die nun alle den selben Wert lesen, dann wieder die Schleife...

Jetzt kommt noch das Sahnehäubchen der Verwirrung. Die Schleife zählt y nicht bis 999 hoch sondern bis 1000, da bricht sie dann ab. Das erklärt warum Threads die nach dem Schleifenende y auslesen die Zahl 1000 sehen.
Mit der Zwischenvariable wird für jeden Thread eine Kopie des Werts angelegt, dann schaut jeder in seine eigene Kopie deren Wert sich nicht mehr ändert.

ByteDevil Themenstarter:in
132 Beiträge seit 2013
vor 7 Jahren

Ja danke euch 😃 Jetzt hab ich es kapiert. War mir nicht bewusst das dort solch eine Verzögerung im Gange ist.

Bei diesem kleinen Beispielcode ist es tatsächlich so, das die Berechnung sogar langsamer wird wenn ich für jede der Zeilen einen neuen Thread starte...war mir aber klar das es mit Kanonen auf Spatzen schießen ist. Das war wie gesagt nur als Beispiel. Meine echte Berechnung ist aufwändiger und dort habe ich tatsächlich die benötigte Zeit pro Berechnung um fast 85% reduzieren können...denn vorher schlief meine CPU fast ein^^

Aber auch unbegrenzt viele Threads öffnen zu lassen war nicht so optimal. Weniger ist auch hier teilweise mehr. Hab es nun so gemacht, dass maximal so viele Berechnungs-Threads gestartet werden, wie CPU-Kerne vorhanden sind (in meinem Fall 12).

Nochmal ein dickes Danke an alle 😃

Grüße,
ByteDevil

C
2.121 Beiträge seit 2010
vor 7 Jahren

Hab es nun so gemacht, dass maximal so viele Berechnungs-Threads gestartet werden, wie CPU-Kerne vorhanden sind (in meinem Fall 12).

Gute Entscheidung. 12 Kerne können nur 12 Dinge gleichzeitig tun. Wenn man dir mehr als eine Aufgabe gleichzeitig zu tun gibt, wirst auch du nur langsamer statt schneller 😃

4.931 Beiträge seit 2008
vor 7 Jahren

Hallo ByteDevil,

als Ergänzung (bzgl. des Verhaltens bei der Variable y) noch für dich die Stichworte: Closure sowie Captured Variable, s. z.B. Closures and Captured Variable C#

16.806 Beiträge seit 2008
vor 7 Jahren

Bitte macht niemals Performance-Vergleiche mit Buffer-Outputs wie Console.Writeline.
Das verfälscht jeglichen Vergleich und verlangsamt die Ausführung um das xx Fache!

Und vergesst bitte nicht: Premature optimization is the root of all evil.
Aber es kann sich bei Parallel Programming schnell lohnen. Und ihr wisst ja: performance is a feature!

Tipps bei Paralleler Verarbeitung:

  • Arbeitet mit Tasks statt mit Threads. Threads haben ihre Vorteile; kommen aber nur bei pauschalen <1% in C# Situationen wirklich zum Tragen. Sie lassen sich einfach viel leichter als Entwickler anwenden.
  • Niemals Kaskadieren. Das beschränkt euch künstlich und es wird langsamer. Verwendet die parallele Schleife wenn möglich in der Hierachieebene oben!
  • Wenn int x == y dann verwendet alles in einer Schleife (Parallel.For) - vielfaches schneller!
  • Es kann schneller sein, wenn ihr die Liste zu iterierenden Objekte bereits aufgebaut habt. Das macht es dem Scheduler hinter Parallel.For/Foreach leichter! Parallel.For vs. Parallel.ForEach kann schneller sein, wenn Hinter ForEach noch der Enumerator was feuern muss.
  • Vermeidet wenn möglich locking innerhalb der parallelen Abarbeitung. Legt euch hier wenn möglich eine lokale Variable an und gibt das Resultate zurück. Das braucht zwar kurz mehr Speicher; aber die Ausführung wird nicht künstlich verlangsamt.

Deswegen ist:

            var matrix = new int[1000, 1000];
            Parallel.ForEach(Enumerable.Range(0,1000), y =>
            {
                for(int x = 0;x < 1000;x++)
                    matrix[x, y] = x * y;
            });

meist, aber nicht immer schneller als:

            var matrix = new int[1000, 1000];
            Parallel.For(0, 1000, y =>
            {
                for(int x = 0;x < 1000;x++)
                    matrix[x, y] = x * y;
            });

Der Faktor wird bestimmt 2-3 betragen!

Hier mal Basiscode, mit dem jeder selbst vergleichen kann.
Mein Dual Xeon mit 16 logischen Prozessoren wird andere Resultate liefern als zB. ein i5 Dual Core und 4 logischen Prozessoren.

using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

namespace mycsharp.Examples.ParallelVsParallel
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            Console.WriteLine("Comparison");

            var runs = 50;

            var runParallelForSeqFirst = Messure(RunWithParallelForSeqFirst, runs);

            var runParallelForParFirst = Messure(RunWithParallelForParFirst, runs);

            var runParallelForXandYSame = Messure(RunWithParallelForXandYSame, runs);

            var runParalellForeach = Messure(RunWithParallelForeach, runs);


            Console.WriteLine($"== Results of {runs} runs");
            Console.WriteLine($" >> Parallel For Seq First: {runParallelForSeqFirst} in total");
            Console.WriteLine($" >> Parallel For Par First: {runParallelForParFirst} in total");
            Console.WriteLine($" >> Parallel For X and Y same: {runParallelForXandYSame} in total");
            Console.WriteLine($" >> Parallel Foreach: {runParalellForeach} in total");

            Console.ReadKey();
        }

        private static Int64 Messure(Action action, int runCount)
        {
            Stopwatch sw = Stopwatch.StartNew();
            for(int i = 0;i < runCount;i++)
            {
                action();
            }
            return sw.ElapsedMilliseconds;
        }


        public static void RunWithParallelForParFirst()
        {
            var matrix = new int[1000, 1000];
            Parallel.For(0, 1000, y =>
            {
                for(int x = 0;x < 1000;x++)
                    matrix[x, y] = x * y;
            });
        }
        public static void RunWithParallelForXandYSame()
        {
            var matrix = new int[1000, 1000];
            Parallel.For(0, 1000, y =>
            {
                var x = y;
                matrix[x, y] = x * y;
            });
        }

        public static void RunWithParallelForSeqFirst()
        {
            var matrix = new int[1000, 1000];
            for(int x = 0;x < 1000;x++)
            {
                var thisX = x; // modified closure
                Parallel.For(0, 1000, y =>
                {
                    matrix[thisX, y] = thisX * y;
                });
            }
        }

        public static void RunWithParallelForeach()
        {
            var matrix = new int[1000, 1000];
            Parallel.ForEach(Enumerable.Range(0, 1000), y =>
            {
                for(int x = 0;x < 1000;x++)
                    matrix[x, y] = x * y;
            });

        }
    }
}