Warum läuft dieser Code synchron?

8

Ich versuche, Nebenläufigkeit zu verstehen, indem ich es im Code mache. Ich habe ein Code-Snippet, von dem ich dachte, dass es asynchron läuft. Aber als ich die Debug-Writerline-Anweisungen eingab, stellte ich fest, dass es synchron lief. Kann jemand erklären, was ich anders machen muss, um ComputeBB () mit Task.Something auf einen anderen Thread zu schieben?

Erläuterung Ich möchte, dass dieser Code ComputeBB in einem anderen Thread ausführt, damit der Hauptthread ohne Blockierung weiterläuft.

Hier ist der Code:

%Vor%

Hier ist die Ausgabe im unmittelbaren Fenster:

%Vor%

Erläuterung Wenn es wirklich asynchron ausgeführt würde, würde dies in dieser Reihenfolge geschehen:

%Vor%

Aber es ist nicht.

Ausarbeitung Der aufrufende Code hat eine Signatur wie folgt: private static async Task loadAsBinaryAsync (string fileName) Auf der nächsten Ebene versuche ich jedoch, die Verwendung von async zu beenden. Also hier ist der Call-Stack von oben nach unten:

%Vor%     
philologon 04.05.2015, 04:52
quelle

2 Antworten

8
  

Ich habe ein Code-Snippet, von dem ich annahm, dass es asynchron lief. Aber als ich die Debug-Writerline-Anweisungen eingab, stellte ich fest, dass sie synchron ablaufen.

await wird zum asynchron Warten eines abgeschlossenen Vorgangs verwendet. Währenddessen gibt es die Kontrolle zurück an die aufrufende Methode bis zum Abschluss.

  

was ich anders machen muss, um ComputeBB () auf einen anderen Thread zu schieben

Es wurde bereits in einem Threadpool-Thread ausgeführt. Wenn Sie nicht asynchron auf "fire and forget" warten möchten, nicht await den Ausdruck. Beachten Sie, dass dies Auswirkungen auf die Ausnahmebehandlung hat. Jede Ausnahme, die innerhalb des angegebenen Delegaten auftritt, wird innerhalb des angegebenen Task festgehalten, wenn Sie nicht await haben, besteht die Chance, dass sie unbehandelt wird.

Bearbeiten:

Sehen wir uns diesen Code an:

%Vor%

Was Sie gerade tun, blockiert synchron, wenn Sie tsk.Result verwenden. Aus irgendeinem Grund rufen Sie Task.Run zweimal an, einmal in jeder Methode. Das ist unnötig. Wenn du deine ptsDTM -Instanz von CreateFromExistingFile zurückgeben willst, musst du await it, es gibt kein Problem damit. Die Ausführung "Feuer und Vergessen" interessiert das Ergebnis überhaupt nicht. Es möchte einfach starten, welche Operation es benötigt, wenn es fehlschlägt oder erfolgreich ist, ist es normalerweise kein Problem. Das ist hier eindeutig nicht der Fall.

Sie müssen so etwas tun:

%Vor%

Und dann brauchen Sie% cc_de% nicht weiter oben in der Aufrufliste, sondern einfach:

%Vor%

Bei Bedarf.

Lesen Sie auch die C # -Namenskonventionen , dem Sie derzeit nicht folgen.

    
Yuval Itzchakov 04.05.2015 05:07
quelle
2
Der Hauptzweck von

await besteht darin, die Synchronität im asynchronen Code wiederherzustellen. Dadurch können Sie die Teile, die synchron und asynchron ausgeführt werden, problemlos partitionieren. Ihr Beispiel ist insofern absurd, als es niemals irgendeinen Vorteil daraus zieht - wenn Sie die Methode gerade direkt aufgerufen haben, anstatt sie in Task.Run und await ging zu verpacken, hätten Sie genau das gleiche Ergebnis (mit weniger Overhead). .

Bedenken Sie dies jedoch:

%Vor%

Auch hier haben Sie die Synchronizität zurück ( await fungiert als Synchronisationsbarriere), aber Sie haben tatsächlich drei unabhängige Operationen asynchron zueinander ausgeführt .

Nun, es gibt keinen Grund, so etwas in Ihrem Code zu tun, da Sie Parallel.ForEach auf der untersten Ebene verwenden - Sie verwenden bereits die CPU bis zum Maximum (mit unnötigem Overhead, aber ignorieren wir das für jetzt).

Also ist die grundlegende Verwendung von await eigentlich eher asynchrone I / O als die CPU-Arbeit - abgesehen von der Vereinfachung von Code, der darauf beruht, dass einige Teile der CPU-Arbeit synchronisiert werden und einige nicht (z Sie haben vier Threads der Ausführung, die gleichzeitig verschiedene Teile des Problems bearbeiten, aber irgendwann müssen sie wiedervereinigt werden, um die einzelnen Teile zu verstehen - schauen Sie sich beispielsweise die Klasse Barrier an. Dazu gehören Dinge wie "Sicherstellen, dass die Benutzeroberfläche nicht blockiert, während einige CPU-intensive Vorgänge im Hintergrund ablaufen" - dies führt dazu, dass die CPU in Bezug auf die Benutzeroberfläche asynchron arbeitet . Aber irgendwann möchten Sie die Synchronität wieder einführen, um sicherzustellen, dass Sie die Ergebnisse der Arbeit auf der Benutzeroberfläche anzeigen können.

Betrachten Sie dieses winforms-Code-Snippet:

%Vor%

(Beachten Sie, dass die Methode async void , nicht async Task oder async Task<T> ) ist

Was passiert, ist, dass (auf dem GUI-Thread) der Beschriftung zuerst der Text Calculating... zugewiesen wird, dann die asynchrone DoTheUltraHardStuff -Methode geplant wird und dann die Methode zurückkehrt. Sofort. Dadurch kann der GUI-Thread alles tun, was er tun muss. Wie auch immer - sobald der asynchrone Task abgeschlossen ist und die GUI den Callback frei hat, wird die Ausführung von btnDoStuff_Click fortgesetzt mit dem result bereits vergeben ( (oder natürlich eine Ausnahme), zurück auf den GUI-Thread, so dass Sie die Bezeichnung auf den neuen Text einschließlich des Ergebnisses der asynchronen Operation setzen können.

Asynchronität ist nicht eine absolute Eigenschaft - das Zeug ist asynchron zu etwas anderem und synchron zu etwas anderem. Es macht nur Sinn in Bezug auf etwas anderes.

Hoffentlich können Sie jetzt zu Ihrem ursprünglichen Code zurückkehren und den Teil verstehen, den Sie vorher falsch verstanden haben. Die Lösungen sind natürlich mehrere, aber sie hängen sehr davon ab, wie und warum Sie versuchen, das zu tun, was Sie tun möchten. Ich vermute, dass du Task.Run oder await überhaupt nicht benutzen musst - die Parallel.ForEach versucht bereits, die CPU-Arbeit über mehrere CPU-Kerne zu verteilen, und das Einzige, was du tun kannst, ist sicherzustellen, dass anderer Code nicht funktioniert Ich muss nicht darauf warten, dass diese Arbeit beendet wird - was in einer GUI-Anwendung sehr sinnvoll wäre, aber ich sehe nicht, wie es in einer Konsolenanwendung mit dem einzigartigen Zweck der Berechnung dieses einzelnen Dinges nützlich wäre.

Also können Sie tatsächlich await für Fire-and-Forget-Code verwenden - aber nur als Teil von Code, der den gewünschten Code nicht verhindert von der Ausführung fortfahren. Zum Beispiel könnten Sie Code wie folgt haben:

%Vor%

Dies erlaubt SomeHardWorkAsync asynchron in Bezug auf DoSomeOtherWorkInTheMeantime , aber nicht in await result auszuführen. Und natürlich können Sie await s in SomeHardWorkAsync verwenden, ohne die Asynchronität zwischen SomeHardWorkAsync und DoSomeOtherWorkInTheMeantime zu verringern.

Das GUI-Beispiel, das ich oben gezeigt habe, nutzt den Vorteil, die Fortsetzung als etwas zu behandeln, das nach dem Abschluss der Aufgabe passiert, während die in der Methode Task erstellte async ignoriert wird (es gibt wirklich nicht viel von a Unterschied zwischen der Verwendung von async void und async Task , wenn Sie das Ergebnis ignorieren . Um beispielsweise Ihre Methode zu aktivieren und zu löschen, können Sie einen Code wie diesen verwenden:

%Vor%

Dies führt dazu, dass DoStuffWithResult ausgeführt wird, sobald result bereit ist, während die Methode Fire selbst unmittelbar nach Ausführung von ProcessFileAsync (bis zum ersten await oder jedem expliziten return someTask ) zurückkehrt. .

Dieses Muster ist normalerweise verpönt - es gibt wirklich keinen Grund, void aus einer asynchronen Methode zurückzugeben (abgesehen von Event-Handlern); Sie könnten genauso einfach Task (oder sogar Task<T> abhängig vom Szenario) zurückgeben und den Anrufer entscheiden lassen, ob er möchte, dass sein Code synchron zu Ihnen ausgeführt wird oder nicht.

Wieder,

%Vor%

macht dasselbe wie async void , außer dass der Aufrufer entscheiden kann, was er mit der asynchronen Task machen soll. Vielleicht möchte er zwei davon parallel starten und weitermachen, nachdem alle fertig sind? Er kann nur await Task.WhenAll(Fire("1"), Fire("2")) .Oder er will nur, dass das völlig asynchron in Bezug auf seinen Code passiert, also ruft er einfach Fire("1") an und ignoriert die resultierende Task (natürlich, idealerweise willst du zumindest mögliche Ausnahmen behandeln).

    
Luaan 04.05.2015 08:13
quelle