Laden...

Parallel.For - Zeit für Threadgenerierung?

Erstellt von Gimmick vor 6 Jahren Letzter Beitrag vor 6 Jahren 1.776 Views
G
Gimmick Themenstarter:in
154 Beiträge seit 2015
vor 6 Jahren
Parallel.For - Zeit für Threadgenerierung?

Hallo,

mir ist bei der Umsetzung der Paralellisierung in meinem Programm aufgefallen, dass es unter gewissen Umständen scheinbar sehr lange dauert bis weitere Threads gestartet werden.

Beispiel:
Eine einfache Parallel.For-Schleife führt 1 bis 4 Berechnungen durch, die ~5 Sekunden (alles grobe Werte) dauern.
1 Rechnung: 5 s
2 Rechnungen: 5,1 s
3 Rechnungen: 5,2 s
4 Rechnungen : 5,3 s

Das ist so wie ich mir das vorgestellt habe ^^.

Aber wenn die einzelnen Rechnungen erheblich längern dauern ändert sich das Bild:

1 Rechnung: 10 s
2 Rechnungen: 20 s
3 Rechnungen : 21 s
4 Rechnungen: 22 s

Warum dauert das Erstellen von weiteren Threads beim ersten mal so lange? Muss da erst was geladen werden und das Laden + Thread erstellen läuft dann auf dem 1. Thread, der schon mit Rechnen beschäftigt ist?

Kann man das irgendwie beschleunigen?

16.842 Beiträge seit 2008
vor 6 Jahren

Parallel.For verwendet Tasks, keine Threads. Das ist ein anderes, abstrakteres Konzept.
Ein Task hat im Hintergrund einen Thread wobei ein Thread mehrere Tasks verwalten kann.

Asynchrones Pipelining wäre vermutlich auch das bessere Konzept als Parallel.For.
Hier machst Du es Dir vermutlich zu einfach 😉

Mit Deinen Messungen alleine wird aber nicht deutlich, was genau die Zeit versursacht.
Wir sehen ja schließlich auch kein Code und damit keinen potentiell vorhandenen Konzeptfehler.

6.911 Beiträge seit 2009
vor 6 Jahren

Hallo Gimmick,

Parallel.For funktioniert grob so:

Zitat von: Is it ok to use nested Parallel.For loops? | Parallel Programming with .NET
Parallel.For begins by creating just one task. When that task is executed, it’ll first queue a replica of itself, and will then enlist in the processing of the loop; at this point, it’s the only task processing the loop. The loop will be processed serially until the underlying scheduler decides to spare a thread to process the queued replica. At that point, the replica task will be executed: it’ll first queue a replica of itself, and will then enlist in the processing of the loop. Starting to see a pattern?

Daher ist es -- wie bei Tasks generell -- wichtig dass es sich um kleine, schnell abzuarbeitende Aufgaben handelt.

Sonst ist es wie Abt schreibt, wir wissen nichts von deinem Problem und können so nicht konkret antworten.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

G
Gimmick Themenstarter:in
154 Beiträge seit 2015
vor 6 Jahren

Hm okay.

Also was ich mache sieht ungefähr so aus:


private async void button1_Click(object sender, EventArgs e)
        {
         await Task.Run(() =>  Taskstuff(box, trackbarvalue, Thresholds));
         }

private void  Taskstuff(bool box, int trackbarvalue, List<string> Thresholds)
         {
           Parallel.For(0, Bitmapsammlung.Count, index =>
                 {
                  DoStuff(Bitmapsammlung[Convert.ToInt32(index)], box, Pfade[Convert.ToInt32(index)], trackbarvalue, Thresholds[Convert.ToInt32(index)]);

                 });
          }

private  void DoStuff(Bitmap Bild, bool box, string path, int trackbarvalue, string Threshold)
        {

             Komplizierter_Kram_der_dauern_kann(Bild,box, path, trackbarvalue, Threshold);
         }

Der Plan ist, alle "DoStuff"s zur Zeitersparnis möglichst gleichzeitig berechnen zu lassen. Die CPU-Last Anzeige in Visual Studio steigt auch passend zur Anzahl an Elementen in Parallel.For, aber es sieht so aus, als würden bei 4 Gesamteinträgen zwei deutlich schneller fertig, als die anderen beiden.
Mich wundert halt auch, dass das bei auch langen - aber eben nicht sooooo langen - Rechnungen nicht so ist.

6.911 Beiträge seit 2009
vor 6 Jahren

Hallo Gimmick,

weißt du welchen Datentyp index hat? Das Convert kannst du dir hier ersparen.

Parallel.For ist an sich schon richtig aufgerufen. Dein Verhalten kannst du da nur per Profiler prüfen, denn ich vermute dass aufgrund von Synchronisieren du dich selbst ausbremst -- aber das kann ich deinem Code nicht übernehmen.

Es kommt hier auch darauf an was Komplizierter_Kram_der_dauern_kann so macht. Wenn es Bitmaps sind kann es auch sein, dass GDI+ (System.Graphics) dich ausbremst, denn dieses arbeitet intern eher sequentiell. Das würde zu deiner Beobachtung passen.

Schau dir Pipelines und das Beispiel darin an, vllt. kannst du deinen "Kram" darauf anpassen, dann sollte es mit der Laufzeit besser gehen.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

G
Gimmick Themenstarter:in
154 Beiträge seit 2015
vor 6 Jahren

weißt du welchen Datentyp index hat? Das Convert kannst du dir hier ersparen.

Hab ich geändert.

Es kommt hier auch darauf an was Komplizierter_Kram_der_dauern_kann so macht. Wenn es Bitmaps sind kann es auch sein, dass GDI+ (System.Graphics) dich ausbremst, denn dieses arbeitet intern eher sequentiell. Das würde zu deiner Beobachtung passen.

In Komplizierter_Kram_der_dauern_kann, wird jeweils ein Bitmap per LockBits-Methode in ein Array kopiert und mit den Koordinaten der Pixel werden dann nach und nach verschiedene Transformationen, Faltungen etc. durchgeführt.

Wenn es Anfangs erst rein seriell wäre, müsste dann nicht die angezeigte CPU-Last auch entsprechend niedriger sein und erst steigen, wenn parallel gerechnet wird?

6.911 Beiträge seit 2009
vor 6 Jahren

Hallo Gimmick,

Wenn es Anfangs erst rein seriell wäre

Beziehst du das auf den zitierten Abschnitt meiner Antwort oben?
Der erste Task der zu Beginn sequentiell abgearbeitet wird, stellt sofort die "Replika" ein und dann macht der seine eigentliche Arbeit. Der TaskScheduler kann, sofern Ressourcen (Threads) verfügbar sind, dann unmittelbar danach mit der Replika beginnen, usw. Der sequentielle Anteil ist i.d.R. zu gering um das zu bemerken.
BTW: CPU-Last lässt sich genau auch nur mit einem Profiler betrachten. Um das Verhalten von Parallel.For genau zu betrachten ist "Instrumentation" nötg, dabei wird bei Methoden-Start ein Marker gesetzt und bei Methoden-Ende wieder ein Marker, damit sich genau feststellen lassen kann was und wann passiert. Allerdings ist dieses "instrumentieren" aufwändig und verlangsamt die Programmausführung. Daher wird meist nur "Sampling" verwendet, d.h. zu bestimmten Intervallen werden die Daten aufgenommen und so lässt sich eine statistische Aussage treffen.

Zurück zum Problem: schau dir das Problem echt einmal mit einem Profiler an -- in VS gibt es dieses eh für alle Editionen. Ich kann mir vorstellen dass das Synchronisationsproblem, davon gehe ich einmal aus, vom GC stammt, da durch LockBits der Speicherbereich für die Bilder gepinnt wird und so der GC ein wenig in seiner Arbeit gestört wird. D.h. als Ergebnis vom Profiler wäre auch interessant ob und wann ein GC-Ereignis (Collection) auftritt.
Sollte dies tatsächlich das Problem sein, so könntest du im Code die Bitmap sofort wieder unlocken, nachdem du das Array erstellt hast, z.B. so:


public static class BitmapExtensions
{
    public static byte[] RgbValues(this Bitmap bitmap)
    {
        if (bitmap == null)
            throw new ArgumentNullException(nameof(bitmap));

        if (bitmap.PixelFormat == (bitmap.PixelFormat | PixelFormat.Indexed))
            throw new ArgumentException("Indexed bitmap not allowed", nameof(bitmap));

        bool isAlphaBitmap = bitmap.PixelFormat == (bitmap.PixelFormat | PixelFormat.Alpha);

        BitmapData bmpData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadWrite, bitmap.PixelFormat);
        IntPtr bmpPtr      = bmpData.Scan0;

        try
        {
            int n            = bitmap.Width * bitmap.Height;
            n                = isAlphaBitmap ? n * 4 : n * 3;
            byte[] rgbValues = new byte[n];

            Marshal.Copy(bmpPtr, rgbValues, 0, rgbValues.Length);

            return rgbValues;
        }
        finally
        {
            bitmap.UnlockBits(bmpData);
        }
    }
}

Vorausgesetzt du brauchst das Bitmap an sich nicht, sondern nur die Pixelwerte.

Ob das wirklich die Ursache ist weiß ich nicht, das ist im Moment nur meine Vermutung.

Du könntest sonst auch gem. [Tutorial] Vertrackte Fehler durch Vergleich von echtem Projekt mit minimalem Testprojekt finden vorgehen. Also dass du den "Kram" durch eine Schleife ersetzt die auch seine Zeit braucht. Dann ist alles mit Bitmap, etc. erstmal draußen und du kannst prüfen ob das mit der parallel-Schleife wenigstens passt 😉

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

G
Gimmick Themenstarter:in
154 Beiträge seit 2015
vor 6 Jahren

Ich unlocke bereits sobald es geht. Das mit dem seriel bezog sich auf die Bitmaps.

Werde mal ein Testprogramm bauen und mir die sache mit einem Profiiler ansehen =)