Async Await für parallelen Code

  • VB.NET
  • .NET 7–8

Es gibt 36 Antworten in diesem Thema. Der letzte Beitrag () ist von Haudruferzappeltnoch.

    Jo. Ich habe mich auch schon wieder länger nicht mit dieser async-await Geschichte befasst.

    Das wäre für mich irgendwie die richtige und logische Vorgehensweise.
    Spoiler anzeigen

    C#-Quellcode

    1. internal partial class Program
    2. {
    3. public static void Main()
    4. {
    5. //TestSeq();
    6. TestParallelAsyncAwait();
    7. //TestParallelOnlyTask();
    8. //TestParallelForEach();
    9. Console.ReadLine();
    10. }
    11. private static void TestSeq()
    12. {
    13. //Se­quen­zi­eller Ablauf.
    14. Thread.Sleep(1000);
    15. Console.WriteLine($"{nameof(TestSeq)}: {DateTime.Now.ToLongTimeString()}");
    16. Console.WriteLine("Start");
    17. //se­quen­zi­eller Ablauf ohne Rücksetzung (using) der Task.
    18. //Vorgehensweise wird oft verwendet, ist aber streng genommen
    19. //nicht die richtige, weil für die sequentielle kein Task
    20. //gebraucht wird.
    21. //Task.Run(HeavyWork1).Wait();
    22. //Task.Run(HeavyWork2).Wait();
    23. HeavyWork1();
    24. HeavyWork2();
    25. Console.WriteLine("End");
    26. Console.WriteLine($"{nameof(TestSeq)}: {DateTime.Now.ToLongTimeString()}");
    27. Console.WriteLine();
    28. }
    29. private async static void TestParallelAsyncAwait()
    30. {
    31. //Alle Task laufen Parallel mit async await
    32. Thread.Sleep(1000);
    33. Console.WriteLine($"{nameof(TestParallelAsyncAwait)}: {DateTime.Now.ToLongTimeString()}");
    34. Console.WriteLine("Start");
    35. using var t1 = Task.Run(HeavyWork1);
    36. using var t2 = Task.Run(HeavyWork2);
    37. // .. oder wenn z.B. CancellationToken verwendet wird.
    38. //await t1.WaitAsync();
    39. //await t2.WaitAsync();
    40. //... oder so
    41. //var tasks = new List<Task> { t1, t2 };
    42. //await Task.WhenAll(tasks);
    43. //... oder so
    44. await Task.WhenAll(t1, t2);
    45. Console.WriteLine("End");
    46. Console.WriteLine($"{nameof(TestParallelAsyncAwait)}: {DateTime.Now.ToLongTimeString()}");
    47. Console.WriteLine();
    48. }
    49. private static void TestParallelOnlyTask()
    50. {
    51. //Alle Task laufen Parallel OHNE async await
    52. Thread.Sleep(1000);
    53. Console.WriteLine($"{nameof(TestParallelOnlyTask)}: {DateTime.Now.ToLongTimeString()}");
    54. Console.WriteLine("Start");
    55. using var t1 = Task.Run(HeavyWork1);
    56. using var t2 = Task.Run(HeavyWork2);
    57. //mit jeweiligen Wait() ...
    58. //t1.Wait(); t2.Wait();
    59. //..Alle Wait() einfach über eine foreach
    60. //Task[] tasks = [ t1, t2 ];
    61. //foreach (var t in tasks)
    62. // t.Wait();
    63. //.. oder die schönste vorgehensweise
    64. Task.WaitAll(t1, t2);
    65. Console.WriteLine("End");
    66. Console.WriteLine($"{nameof(TestParallelOnlyTask)}: {DateTime.Now.ToLongTimeString()}");
    67. Console.WriteLine();
    68. }
    69. private static void TestParallelForEach()
    70. {
    71. //Alle Task laufen Parallel OHNE async-await
    72. Thread.Sleep(1000);
    73. Console.WriteLine($"{nameof(TestParallelForEach)}: {DateTime.Now.ToLongTimeString()}");
    74. Console.WriteLine("Start");
    75. using var t1 = Task.Run(HeavyWork1);
    76. using var t2 = Task.Run(HeavyWork2);
    77. Task[] tasks = [t1, t2];
    78. //Da Task die 'Task.WhenAll' besitzt,
    79. //macht die Parallel.ForEach keinen Sinn.
    80. //Möglich ist die vorgehensweise aber.
    81. var plr = Parallel.ForEach(tasks, t =>
    82. {
    83. t.Wait();
    84. });
    85. //while (!plr.IsCompleted) ;
    86. Console.WriteLine("End");
    87. Console.WriteLine($"{nameof(TestParallelForEach)}: {DateTime.Now.ToLongTimeString()}");
    88. Console.WriteLine();
    89. }
    90. private static void HeavyWork1()
    91. {
    92. Console.WriteLine($"{nameof(HeavyWork1)} Before: {DateTime.Now.ToLongTimeString()}");
    93. Thread.Sleep(1000);
    94. Console.WriteLine($"{nameof(HeavyWork1)} After: {DateTime.Now.ToLongTimeString()}");
    95. }
    96. private static void HeavyWork2()
    97. {
    98. Console.WriteLine($"{nameof(HeavyWork2)} Before: {DateTime.Now.ToLongTimeString()}");
    99. Thread.Sleep(1000);
    100. Console.WriteLine($"{nameof(HeavyWork2)} After: {DateTime.Now.ToLongTimeString()}");
    101. }
    102. }

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „exc-jdbi“ () aus folgendem Grund: Bisserl ausgehübst

    Der Code in Post#15 ergibt, nach Ergänzung, dass in Test1 die Tasks - entsprechend Wait - nacheinander abgearbeitet werden, in Test 2 gleichzeitig. Ok, da ist jetzt unklar, warum. Es macht ja quasi das gleiche. Ob jetzt

    Quellcode

    1. A()
    2. B()
    oder

    VB.NET-Quellcode

    1. For Each C In {A, B}
    2. C()
    3. Next
    Ich verstehe das Problem langsam.

    ##########

    VB.NET-Quellcode

    1. Dim A = Task.Run(AddressOf HeavyWork1)
    2. Dim B = Task.Run(AddressOf HeavyWork2)
    3. A.Wait()
    4. B.Wait()

    Das führt zum gleichen Ergebnis wie die For-Schleife. Beide Tasks werden gleichzeitig gestartet (also nicht nur vorbereitet) und B.Wait wird erreicht, wenn ein Task fertig ist.
    Bilder
    • Console.png

      9,97 kB, 635×344, 32 mal angesehen
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.

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

    Haudruferzappeltnoch schrieb:

    Task.Run(AddressOf HeavyWork1).Wait
    Task.Run(AddressOf HeavyWork2).Wait


    Haudruferzappeltnoch schrieb:

    For Each t In {Task.Run(AddressOf HeavyWork1), Task.Run(AddressOf HeavyWork2)}
    t.Wait()
    Next



    sind leider nicht das gleiche weil in der foreach wird eine Array<Task> abgearbeitet, und in der oben ist es die Wait-Methode, also eine void-Methode die nacheinander aufgerufen wird ohne vorher je ein Task-Datentyp erzeugt zu haben.

    So Task.Run(AddressOf HeavyWork2).Wait wird also nie vorab ein Task-Datentyp erstellt. Der ist aber anscheinend notwending, um alle Wait-Methoden zusammenzuführen um es nachher Parallel wie es in der Foreach gemacht wird auszuführen.

    Gruss exc-jdbi

    VB.NET-Quellcode

    1. For Each t In {Task.Run(AddressOf HeavyWork1), Task.Run(AddressOf HeavyWork2)}
    2. t.Wait()
    3. Next
    Zeile#1 erzeugt 2 Tasks, und die rennen schon.
    In zeile#2 wird zunächstmal auf Task1 gewartet.
    Wenn er durch ist wird direkt danach auf Task2 gewartet. Der ist aber schon die ganze Zeit gelaufen, und deshalb ist die Rest-Wartezeit natürlich kürzer.
    Bzw. wenn Task2 schon fertig ist, wenn auf ihn gewartet werden soll ist die Rest-Wartezeit 0.

    Insgesamt wird also bei dieser Konstruktion auf den langsamsten der beiden Tasks gewartet, egal obs Task1 ist oder Task2.
    Dank der Erkenntnisse, die ihr mir vermitteln konntet, habe ich nun die Async/Await Version gefunden (also kein Run, WhenAny, WhenAll):

    VB.NET-Quellcode

    1. Module Program
    2. Sub Main()
    3. HeavyWorkReihe()
    4. HeavyWorkParallel()
    5. Console.ReadLine()
    6. End Sub
    7. Private Sub HeavyWorkReihe()
    8. Thread.Sleep(1000)
    9. Console.WriteLine($"{NameOf(Test1)}: {Date.Now.ToLongTimeString()}")
    10. Console.WriteLine("Start")
    11. HeavyWork1()
    12. HeavyWork2()
    13. Console.WriteLine("End")
    14. Console.WriteLine($"{NameOf(Test1)}: {Date.Now.ToLongTimeString()}")
    15. Console.WriteLine()
    16. End Sub
    17. Private Sub HeavyWorkParallel()
    18. Thread.Sleep(1000)
    19. Console.WriteLine($"{NameOf(Test2)}: {Date.Now.ToLongTimeString()}")
    20. Console.WriteLine("Start")
    21. Dim A = HeavyWork1Async()
    22. Dim B = HeavyWork2Async()
    23. A.Wait()
    24. B.Wait()
    25. Console.WriteLine("End")
    26. Console.WriteLine($"{NameOf(Test2)}: {Date.Now.ToLongTimeString()}")
    27. Console.WriteLine()
    28. End Sub
    29. Private Sub HeavyWork1()
    30. HeavyWork1Async.Wait()
    31. End Sub
    32. Private Sub HeavyWork2()
    33. HeavyWork2Async.Wait()
    34. End Sub
    35. Private Async Function HeavyWork1Async() As Task
    36. Console.WriteLine($"{NameOf(HeavyWork1)} Before: {Date.Now.ToLongTimeString()}")
    37. Await Task.Delay(1000)
    38. Console.WriteLine($"{NameOf(HeavyWork1)} After: {Date.Now.ToLongTimeString()}")
    39. End Function
    40. Private Async Function HeavyWork2Async() As Task
    41. Console.WriteLine($"{NameOf(HeavyWork2)} Before: {Date.Now.ToLongTimeString()}")
    42. Await Task.Delay(1000)
    43. Console.WriteLine($"{NameOf(HeavyWork2)} After: {Date.Now.ToLongTimeString()}")
    44. End Function
    45. End Module

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

    jo, und nu probierma so:

    VB.NET-Quellcode

    1. Private Async Sub HeavyWorkParallel2()
    2. Thread.Sleep(1000)
    3. Console.WriteLine($"{NameOf(HeavyWorkParallel2)}: {Date.Now.ToLongTimeString()}")
    4. Console.WriteLine("Start")
    5. Await Task.WhenAll(HeavyWork1Async(), HeavyWork2Async())
    6. Console.WriteLine("End")
    7. Console.WriteLine($"{NameOf(HeavyWorkParallel2)}: {Date.Now.ToLongTimeString()}")
    8. Console.WriteLine()
    9. End Sub
    Funktioniert, aber hier kommt jetzt der Punkt den ich immer noch nicht verstehe:
    Das funktioniert auch:

    VB.NET-Quellcode

    1. Private Sub HeavyWorkParallel2()
    2. Thread.Sleep(1000)
    3. Console.WriteLine($"{NameOf(HeavyWorkParallel2)}: {Date.Now.ToLongTimeString()}")
    4. Console.WriteLine("Start")
    5. Task.WhenAll(HeavyWork1Async(), HeavyWork2Async()).Wait()
    6. Console.WriteLine("End")
    7. Console.WriteLine($"{NameOf(HeavyWorkParallel2)}: {Date.Now.ToLongTimeString()}")
    8. Console.WriteLine()
    9. End Sub
    Wo ist da jetzt noch der Unterschied?
    Und ich finde es auch ganz unintuitiv, dass ich aus dem Ausführen von Tasks wieder eine neue Task mache, was die WhenAll, Run, etc. Methoden ja tun. Ist das nicht doppelt gemoppelt?
    Await Task entspricht Task.Wait. Await kam einfach syntaktisch später.

    ##########

    hab's nicht komplett gelesen, aber hier ein Artikel dazu
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.

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

    Das ist genau der Punkt wo ich Unterschiede zwischen Consolenanwendungen oder Windowsprogrammen, obwohl ich diese Unterscheidung nicht unbedingt gerne mache.

    Das von EDR in #29 gezeigte Beispiel funkst nämlich in einem Windowsprogramm, weil dort das Fenster nach dem aufgebaut konstant auf dem Bildschirm gezeigt wird.

    In der Consolenanwendung funkst das nur, wenn ein Console.ReadLine() oder etwas dergleichen vorliegt. Ich stell mir das immer vor, wie ein sehr grosser Winderstand der das Ausführen von noch nicht ausgeführten Task zwingt, endlich ausgeführt zu werden. (Ähnlich wie DoEvent).

    Ein weiteres Vorteil den ich im Zusammenhang mit async-await nutzen will, ist die Entscheidung, wann und vorallem wo, ein Task komplett fertig ausgeführt wird. Da Void-Methoden keinen Rückgabewert haben, kann ich als Entwickler nicht direkt entscheiden wo und wann der Task komplett fertig ausgeführt wird. Die Ausführung muss nämlich in der entsprechenden Void-Methode direkt gemacht werden. (ausser man arbeitet mit Fenster dann kann wie der Code von EDR zeigt der Task auch so zum direkten Ausführen gebracht werden, dass funkst dann mit der deklaration von await, und die Wait()-Methode muss nicht mehr erwähnt werden.)

    EDIT: Wer sich Async/Await in Anwendung einer Console anschauen will, ich habe hier das Generieren von Primes so umgesetzt.
    Power Fast Prime Generator

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „exc-jdbi“ ()

    VaporiZed schrieb:

    Await Task entspricht Task.Wait. Await kam einfach syntaktisch später.
    Nein. Task.Wait blockiert den aktuellen Thread. Da geht dann nichts mehr - eingefrorene Bildschirm. In einer Console mag das sehr erwünscht sein.
    Await returnt die Async-Methode vorzeitig.sodasss der MainThread nicht blockiert.
    Wenn der NebenThread dann durch ist, springt er wieder an die Await-Stelle und macht danach weiter.
    Kaum zu glauben, schwer zu verstehen, aber in Winforms ungemein praktisch.
    Hier isses richtig erklärt: codeproject.com/Articles/10296…ithout-any-additional-Lin
    Folgende Schleife:

    VB.NET-Quellcode

    1. Dim Tasks As New List(Of Task)
    2. For Each dest In {New FileInfo(path1), New FileInfo(path2)}
    3. Tasks.Add(Task.Run(Sub()
    4. For Each request In Requests
    5. SendTo(dest, request)
    6. Next
    7. End Sub))
    8. Next
    9. Await Task.WhenAll(Tasks)
    10. For Each t In Tasks
    11. t.Dispose()
    12. Next
    gibt es da was dran zu verbessern?
    Also die Requests laufen anständig synchron und die Destinations parallel.

    Haudruferzappeltnoch schrieb:

    gibt es da was dran zu verbessern?


    Lass das Dispose noch weg, Tasks müssen nicht Disposed werden:

    Do I need to dispose of Tasks?
    Here’s my short answer to this question:
    “No. Don’t bother disposing of your tasks.”