none
Parallel.ForEach Problem - Performance RRS feed

  • Frage

  • Ich lese von einer Datenbank ca. 4000 Objekte. In einer Foreach-Schleife konvertiere
    ich 
    Werte, berechne, lese Weitere Daten aus einer Datenbank nach usw Am Ende schreibe ich das in eine Ausgabeliste.
    Mit einer Parallel.ForEach Schleife wollte ich das schneller lösen. Doch das Gegenteil ist der Fall. Es dauert dadurch extrem viel länger.
    Wie kann ich die Bearbeitung der ca. 4000 Objekte beschleunigen? Ist dies der falsche Ansatz?

    Liste <AusgabeView> Ausgabe = new List <AusgabeView> ();

    List <ObjectsView> Records = Datenbank.Objects.OrderBy (x => x.Name) .ToList ();

    Parallel.ForEach (Records, (Item) =>
     {

        AusgabeView oItem = new AusgabeView ();

        oitem.Name = Item.Name

        .....

        // Weitere Daten lesen, mappen, berechnen ect.

        Ausgabe.Add (oItem);

    });


    Freitag, 8. Februar 2019 16:29

Antworten

  • Hi,

    insbesondere der Punkt "lese weitere Daten aus einer Datenbank" sollte überdacht werden.

    In der Regel macht es wenig Sinn, x tausend mal in die Datenbank zu gehen und dann einzelne Daten auszulesen.

    Ich würde es eher so machen, dass Du Dir bspw. eine List<T> erstellst, alle Schlüsselwerte (bspw. die benötigen ForeignKeys) in die Liste einträgst, die untergeordneten Datensätze dann alle auf einmal bspw. per SELECT ... WHERE ID IN ( <WertelisteKommaGetrennt> ) aus der Datenbank lädst und dann die daraus resultierenden Objekte in einer zweiten Schleife jedem Elternelement zuordnest.

    Das kann die Performance um den Faktor, 10, 20, 100, ... verbessern.


    Gruß, Stefan
    Microsoft MVP - Visual Developer ASP/ASP.NET (2001-2018)
    https://www.asp-solutions.de/ - IT Beratung, Softwareentwicklung, Remotesupport

    Freitag, 8. Februar 2019 17:47
    Moderator
  • Hi,

    abgesehen davon, dass Stefans Antwort zu favorisieren ist, müsste man bei Parallel.ForEach() zusätzlich lock verwenden, da List<T>.Add() nicht Thread-sicher ist.

    Gruß

    Heiko

    Freitag, 8. Februar 2019 18:33
  • Hi,
    hier mal eine Demo, in der das Laden der Daten aus einer Datenbank Satz für Satz simuliert wird analog ExecuteReader und dazu eine Verarbeitung jedes Datensatzes mit Zeitaufwand (10 ms).

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace ConsoleApp1
    {
      class Program01
      {
        static void Main(string[] args)
        {
          try
          {
            Demo c = new Demo();
            c.Execute();
          }
          catch (Exception ex) { Console.WriteLine(ex.ToString()); }
          Console.WriteLine("Fertig, Abschluss mit beliebiger Taste");
          Console.ReadKey();
        }
    
        class Demo
        {
          internal void Execute()
          {
            Console.WriteLine("--- Einlesen mit Verarbeitung");
            Stopwatch sw = new Stopwatch();
            sw.Start();
            foreach (var item in GetData()) Verarbeitung(item);
            sw.Stop();
            Console.WriteLine($" Dauer {sw.ElapsedMilliseconds} ms");
            //
            Console.WriteLine("--- Einlesen und dann parallel verarbeiten.");
            sw.Reset();
            sw.Start();
            List<Data> liste = new List<Data>();
            foreach (var item in GetData()) liste.Add(item);
            // parallele Verarbeitung
            Parallel.ForEach(liste, d => { Verarbeitung(d); });
            sw.Stop();
            Console.WriteLine($" Dauer {sw.ElapsedMilliseconds} ms");
          }
    
          internal void Verarbeitung(Data d)
          {
            // Simulation der Dauer der Verarbeitung
            Thread.Sleep(10);
            d.B = d.A;
          }
    
          private IEnumerable<Data> GetData()
          {
            for (int i = 1; i <= 4000; i++)
            {
              Data d = new Data() { ID = i, A = $"Zeile {i}" };
              yield return d;
            }
          }
        }
        internal class Data
        {
          internal int ID { get; set; }
          internal string A { get; set; }
          internal string B { get; set; }
        }
      }
    }

    Und dazu das Messergebnis:

    --- Einlesen mit Verarbeitung
     Dauer 42853 ms
    --- Einlesen und dann parallel verarbeiten.
     Dauer 7868 ms
    Fertig, Abschluss mit beliebiger Taste


    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP für Developer Technologies)
    Meine Homepage mit Tipps und Tricks

    Donnerstag, 21. Februar 2019 08:41
  • Hi,
    ergänzend zu den anderen Antworten gibt es für mich die Frage: "Wozu benötigst Du diese Daten im Client?".

    Wenn Du die Daten für weitere Berechnungen bzw. Auswertungen benötigst, wäre bestimmt eine Stored Procedure angebracht, die die Daten im Datenbankserver vorbereitet, so dass nur eine begrenzte Datenmenge an den Client zu übertragen ist.

    Wenn die Daten für die Anzeige benötigt werden, dann werden zu einem Zeitpunkt bestimmt nicht 4000 Datensätze benötigt, sondern nur ein kleiner Teil davon (z.B. 40). Die restlichen Daten können dann bei Bedarf (z.B. Scrollen) oder auch asynchron versetzt nachgeladen werden.


    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP für Developer Technologies)
    Meine Homepage mit Tipps und Tricks

    Samstag, 9. Februar 2019 08:45

Alle Antworten

  • Hi,

    insbesondere der Punkt "lese weitere Daten aus einer Datenbank" sollte überdacht werden.

    In der Regel macht es wenig Sinn, x tausend mal in die Datenbank zu gehen und dann einzelne Daten auszulesen.

    Ich würde es eher so machen, dass Du Dir bspw. eine List<T> erstellst, alle Schlüsselwerte (bspw. die benötigen ForeignKeys) in die Liste einträgst, die untergeordneten Datensätze dann alle auf einmal bspw. per SELECT ... WHERE ID IN ( <WertelisteKommaGetrennt> ) aus der Datenbank lädst und dann die daraus resultierenden Objekte in einer zweiten Schleife jedem Elternelement zuordnest.

    Das kann die Performance um den Faktor, 10, 20, 100, ... verbessern.


    Gruß, Stefan
    Microsoft MVP - Visual Developer ASP/ASP.NET (2001-2018)
    https://www.asp-solutions.de/ - IT Beratung, Softwareentwicklung, Remotesupport

    Freitag, 8. Februar 2019 17:47
    Moderator
  • Hi,

    abgesehen davon, dass Stefans Antwort zu favorisieren ist, müsste man bei Parallel.ForEach() zusätzlich lock verwenden, da List<T>.Add() nicht Thread-sicher ist.

    Gruß

    Heiko

    Freitag, 8. Februar 2019 18:33
  • Hi,
    ergänzend zu den anderen Antworten gibt es für mich die Frage: "Wozu benötigst Du diese Daten im Client?".

    Wenn Du die Daten für weitere Berechnungen bzw. Auswertungen benötigst, wäre bestimmt eine Stored Procedure angebracht, die die Daten im Datenbankserver vorbereitet, so dass nur eine begrenzte Datenmenge an den Client zu übertragen ist.

    Wenn die Daten für die Anzeige benötigt werden, dann werden zu einem Zeitpunkt bestimmt nicht 4000 Datensätze benötigt, sondern nur ein kleiner Teil davon (z.B. 40). Die restlichen Daten können dann bei Bedarf (z.B. Scrollen) oder auch asynchron versetzt nachgeladen werden.


    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP für Developer Technologies)
    Meine Homepage mit Tipps und Tricks

    Samstag, 9. Februar 2019 08:45
  • Vielen Dank für die Antwort. Versuche es so umzusetzen
    Mittwoch, 20. Februar 2019 15:25
  • Ist leider eine SQLite, deshalb kein Stored Procedure möglich
    Mittwoch, 20. Februar 2019 15:26
  • Hi,
    aus Deiner Antwort vermute ich, dass es Dir nicht um Anzeige, sondern um Verarbeitung geht. In diesem Fall kannst Du die 4000 Datensätze "schnell" einlesen, ohne sie zu verarbeiten. Dann startest Du die parallele Verarbeitung mit den eingelesenen Daten.

    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP für Developer Technologies)
    Meine Homepage mit Tipps und Tricks

    Donnerstag, 21. Februar 2019 05:30
  • Some of the requierement we are using the  parallel . Foreach  method, But some times it wants to create some  problem
    • Bearbeitet Carlaya22 Donnerstag, 21. Februar 2019 06:07
    Donnerstag, 21. Februar 2019 06:06
  • Hi,
    hier mal eine Demo, in der das Laden der Daten aus einer Datenbank Satz für Satz simuliert wird analog ExecuteReader und dazu eine Verarbeitung jedes Datensatzes mit Zeitaufwand (10 ms).

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace ConsoleApp1
    {
      class Program01
      {
        static void Main(string[] args)
        {
          try
          {
            Demo c = new Demo();
            c.Execute();
          }
          catch (Exception ex) { Console.WriteLine(ex.ToString()); }
          Console.WriteLine("Fertig, Abschluss mit beliebiger Taste");
          Console.ReadKey();
        }
    
        class Demo
        {
          internal void Execute()
          {
            Console.WriteLine("--- Einlesen mit Verarbeitung");
            Stopwatch sw = new Stopwatch();
            sw.Start();
            foreach (var item in GetData()) Verarbeitung(item);
            sw.Stop();
            Console.WriteLine($" Dauer {sw.ElapsedMilliseconds} ms");
            //
            Console.WriteLine("--- Einlesen und dann parallel verarbeiten.");
            sw.Reset();
            sw.Start();
            List<Data> liste = new List<Data>();
            foreach (var item in GetData()) liste.Add(item);
            // parallele Verarbeitung
            Parallel.ForEach(liste, d => { Verarbeitung(d); });
            sw.Stop();
            Console.WriteLine($" Dauer {sw.ElapsedMilliseconds} ms");
          }
    
          internal void Verarbeitung(Data d)
          {
            // Simulation der Dauer der Verarbeitung
            Thread.Sleep(10);
            d.B = d.A;
          }
    
          private IEnumerable<Data> GetData()
          {
            for (int i = 1; i <= 4000; i++)
            {
              Data d = new Data() { ID = i, A = $"Zeile {i}" };
              yield return d;
            }
          }
        }
        internal class Data
        {
          internal int ID { get; set; }
          internal string A { get; set; }
          internal string B { get; set; }
        }
      }
    }

    Und dazu das Messergebnis:

    --- Einlesen mit Verarbeitung
     Dauer 42853 ms
    --- Einlesen und dann parallel verarbeiten.
     Dauer 7868 ms
    Fertig, Abschluss mit beliebiger Taste


    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP für Developer Technologies)
    Meine Homepage mit Tipps und Tricks

    Donnerstag, 21. Februar 2019 08:41