Testprogramm zur Demonstration der ThreadPool-Funktionalität

    • C#
    • .NET (FX) 4.0

    Es gibt 6 Antworten in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

      Testprogramm zur Demonstration der ThreadPool-Funktionalität

      In meinem Testprogramm wird das Funktionieren des ThreadPools demonstriert.
      In den ThreadPool können max. 64 Threads gepackt werden (Systemgrenze, bei größeen Werten kommt eine entsprechende Exception).
      Zu jedem Thread wird eine ManualResetEvent-Instanz angelegt, mit der auf ein Signal aller Threads gewartet werden kann.
      Es kann vorgegeben werden, auf wieviel Kernen die Threads arbeiten sollen, hier wird (n-1) vorgegeben, dies wird mit

      C#-Quellcode

      1. Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(all);
      gemacht, wobei all eine Variable ist, bei der für jeden zu nutzenden Kern das kommunizierende Bit gesetzt ist.
      Je nach dem, wo (d.h. wann) in der Threadprozedur das Reset-Event gesetzt wird, wird auf den jeweiligen Zeitpunkt (hier - Checkbos-schaltbar: Start oder Beendigung) getriggert und das Programm setzt seinen Lauf fort.
      Über entsprechende Testausgaben kann der Ablauf verfolgt werden, dies erfolgt über die Konsole (Ausgabe - Debuggen).
      Ein Invoken in den Main-Thead ist nicht möglich, da das Warten auf das Sync-Signal die GUI anhält.
      Um den Threads etwas zu tun zu geben, lasse ich sie ein paar Math.Atan2()-Aufrufe abarbeiten, was dazu führt, dass alle benutzte Kerne in die 100%-Auslastung gehen. :thumbsup:
      Bilder
      • CPU.png

        48,29 kB, 820×819, 172 mal angesehen
      Dateien
      Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
      Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
      Ein guter .NET-Snippetkonverter (der ist verfügbar).
      Programmierfragen über PN / Konversation werden ignoriert!
      2. Streich.
      Auf dieser Basis habe ich das ThreadPool-Handling in einen BackgroundWorker gepackt, dieser ist dafür perfekt geeignet.
      Außerdem kann die Anzahl der verwendeten Prozessor-Kerne mit einem NumericUpDown vorgegeben werden (1...n).
      Das Resultat: Die GUI wird nicht blockiert und die Testausgaben kommen in einer TextBox an.
      Und das Beste: Das ganze funktioniert bei einer CPU-Auslastung um 100 % der verwendeten Kerne.
      Bei meinen 4 Kernen hab ich folgende Zeiten gestoppt:
      1 -> 16 s
      2 -> 13 s
      3 -> 12 s
      4 -> 13 s
      Bei 4 (allen) Kernen sieht auch der Ablauf im Taskmanager unruhiger aus, so dass ich die Verwendung von (n-1) Kernen empfehle.
      Dateien
      Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
      Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
      Ein guter .NET-Snippetkonverter (der ist verfügbar).
      Programmierfragen über PN / Konversation werden ignoriert!
      den ersten Streich konnte ich garnet mit allen Cores ausführen, denn ich hab nur 2.

      Beim 2. gings dann.
      Allerdings spinnt iwie die Progressbar - die geht sofort auf fast Maximum.

      V.a. finde ich deinen Code überkompliziert: man braucht den Threadpool nicht direkt anzufassen, und man braucht auch keine ResetEvents (und den BGW schon lange nicht!), und man kann auch beim Threading typisiert und ordentlich gekapselt programmieren.
      Selbst Fork-Join-Threading kann man abhandeln mit typisierter Parameter-Übergabe (und wenn man wollte sogar mit Auswertung von Rückgabewerten).
      Und alles was man braucht, bringen die Delegaten, die man sowieso braucht, selber mit - nämlich die Methoden .BeginInvoke() und .EndInvoke():

      C#-Quellcode

      1. public partial class frmTypedThreading : Form {
      2. // Dummy-Variable, die der Compiler nicht wegoptimiert
      3. public double dummy;
      4. private Stopwatch duration = new Stopwatch();
      5. private readonly Action<string> _Logger;
      6. public frmTypedThreading() {
      7. this.InitializeComponent();
      8. this.numericUpDown1.Minimum = 1;
      9. this.numericUpDown1.Maximum = Environment.ProcessorCount;
      10. this.numericUpDown1.Value = Environment.ProcessorCount - 1;
      11. _Logger = textBox1.AppendText; // ein Delegat!
      12. }
      13. private void button1_Click(object sender, EventArgs e) {
      14. var MaxNbThreads = 64;
      15. this.textBox1.Clear();
      16. this.progressBar1.Value = 0;
      17. this.progressBar1.Maximum = MaxNbThreads;
      18. // für jeden zu benutzenden Kern ein Bit setzen
      19. var flagCores = (int)Math.Pow(2, (double)this.numericUpDown1.Value) - 1;
      20. // Bit-Flag für ProcessorAffinity
      21. //start eines Threadpool-Threads
      22. Action<int, int> allWorker = DoAllWork;
      23. allWorker.BeginInvoke(MaxNbThreads, flagCores, allWorker.EndInvoke, null);
      24. }
      25. private void DoAllWork(int nThreads, int flagCores) {
      26. this.duration.Restart();
      27. Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(flagCores);
      28. //fork-join-threading: erst den delegaten mehrfach mit BeginInvoke() starten, dann alle IAsyncResults EndInvoken.
      29. //EndInvoke blockiert, bis der Thread fertig ist, sodass die Schleife insgesamt wartet, bis der letzte fertig.
      30. Action<int> act = DoWork;
      31. IAsyncResult[] arss = Enumerable.Range(0, nThreads).Select(i => act.BeginInvoke(i, null, null)).ToArray();
      32. foreach (var ar in arss) act.EndInvoke(ar);
      33. BeginInvoke(_Logger, string.Format("done in {0}", duration.Elapsed));
      34. }
      35. private void DoWork(int i) {
      36. BeginInvoke(_Logger, string.Format("start {0} - ", i));
      37. for (int j = 0; j < 10000000; j++) {
      38. this.dummy = Math.Atan2(i, i + 100);
      39. }
      40. BeginInvoke(_Logger, string.Format("end {0} - \n", i));
      41. }
      42. }
      Du siehst: Dadurch, dass ich maxThreads und flagCores als Parameter übergebe brauche ich keine klassenweite Variablen oder Konstanten dafür.
      Und auch die einzelne ThreadNummer brauche ich nicht mittels Convert nach int zu konvertieren, sondern mein DoWork(int) kriegt den Parameter typisiert übergeben, wie sich gehört. Und ebenso lassen sich auch mehrere Params übergeben - wie in DoAllWork(int, int) gezeigt.

      Fürs Logging hab ich nicht sone umständliche Methode gemacht, die erst InvokeRequired testet, und sich dann nochmal aufruft, sondern ich hab einfach statt einer Methode eine Klassenvariable, die einen Delegaten enthält.
      Den kann ich ebenso ausführen wie eine Methode, und kann ich auch direkt BeginInvoken - ich weiß ja von vornherein, dass Invoking required ist.
      Dateien

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „ErfinderDesRades“ ()

      ErfinderDesRades schrieb:

      Allerdings spinnt iwie die Progressbar
      Nö, tut sie nicht.
      Da ist ein deutlicher Unterschied zu sehen, wenn das ganze nur auf einem Core läuft.
      -------------
      Du jast vergessen, die ProcessorAffinity zu setzen. ;)
      Habs mal getestet, es genügt, dies einmal im aufrufenden Thread zu setzen.
      -------------
      Bei Deinem Programm wartet der aufrufende Thread, bis sich alle Sub-Threads beendet haben.
      Bei mir kann sie Sync-Position mit this.doneEvents[i].Set() vorgegeben werden. Diese Synchronisationsmethode halte ich für sehr wichtig.
      Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
      Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
      Ein guter .NET-Snippetkonverter (der ist verfügbar).
      Programmierfragen über PN / Konversation werden ignoriert!
      ich habs jetzt nochmal probiert - bei mir spinnt das Ding - egal bei wieviele Cores.

      ups - nachgebessert :saint:

      RodFromGermany schrieb:

      Bei Deinem Programm wartet der aufrufende Thread, bis sich alle Sub-Threads beendet haben.
      Bei deinem doch auch - so wies derzeit gecodet ist - oder?
      Aber prinzipiell ist natürlich richtig, dasses noch kompliziertere Threading-Szenarien gibt, wo man auf WaitHandles und Kram nicht verzichten kann.
      Nur das normale Threading, ums Gui zu entblocken, und auch das übliche Fork-Join-Threading - bei diesen Szenarien ist der Kram nicht nötig.

      Ich find das im Übrigen sehr unübersichtlich, da gibts Monitor, ManualResetEvent, ManualResetEventSlim, AutoresetEvent, Sempaphore, SemaphoreSlim, WaitHandle - etwa warum benutzst du ManualResetEvent und nicht AutoResetEvent, und warum nicht ManualResetEventSlim?

      Edit: Hab glaub auch gefunden, warum die PB spinnt: Du hast vergessen ManualResetEvent.WaitOne() aufzurufen.
      Aber ergibt in diesem Szenario eben doch keinen Sinn, also da wird ein Nebenthread gestartet, der eigene gestoppt und gewartet, bis der Nebenthread ihn freigibt, um dann- uff, uff die PB zu incrementieren.
      Einfacher wäre wohl, der Nebenthread würde die PB selbst incrementieren.

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „ErfinderDesRades“ ()

      ErfinderDesRades schrieb:

      so wies derzeit gecodet ist
      Ist im 1. Post an eine CheckBox gekoppelt.
      Die ProgressBar ist nur zur Veranschaulichung, die kommt eh nicht vor.
      Das ManualResetEvent ist in meinem Projekt vorgegeben, deswegen hab ich die Untersuchungen gemacht.
      Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
      Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
      Ein guter .NET-Snippetkonverter (der ist verfügbar).
      Programmierfragen über PN / Konversation werden ignoriert!

      RodFromGermany schrieb:

      Das ManualResetEvent ist in meinem Projekt vorgegeben, deswegen hab ich die Untersuchungen gemacht.
      Ah! Du kannst übrigens glaub ebensogut mit nur einem ManualResetEvent auskommen. Das kann man ja mit Reset im aufruferthread erneut sperren.
      Auch kannst du mal AutoResetEvent probieren, das muss man glaub nichtmal jedesmal neu Reseten (jdfs. dem Namen nach).