Wie hilft die asynchrone Unterstützung von C # 5 bei Problemen mit der Synchronisierung von UI-Threads?

16

Ich habe irgendwo gehört, dass das asynchrone Warten auf C # 5 so großartig sein wird, dass Sie sich darüber keine Sorgen machen müssen:

if (InvokeRequired)
{
    BeginInvoke(...);
    return;
}
// do your stuff here

Es sieht so aus, als würde der Rückruf einer Warteoperation im ursprünglichen Thread des Aufrufers erfolgen. Mehrfach wurde von Eric Lippert und Anders Hejlsberg festgestellt, dass diese Funktion darauf zurückzuführen ist, dass Benutzeroberflächen (insbesondere Benutzeroberflächen für Touchscreens) schneller reagieren müssen.

Ich denke, eine übliche Verwendung eines solchen Features wäre etwa so:

public class Form1 : Form
{
    // ...
    async void GetFurtherInfo()
    {
        var temperature = await GetCurrentTemperatureAsync();
        label1.Text = temperature;
    }
}

Wenn nur ein Rückruf verwendet wird, löst das Festlegen des Bezeichnungstextes eine Ausnahme aus, da er nicht im Thread der Benutzeroberfläche ausgeführt wird.

Bisher konnte ich keine Ressource finden, die dies bestätigt. Weiß jemand davon? Gibt es Dokumente, die technisch erklären, wie dies funktioniert?

Bitte geben Sie einen Link von einer zuverlässigen Quelle an, antworten Sie nicht einfach mit "Ja".

Alex
quelle
Dies erscheint zumindest in awaitBezug auf die Funktionalität höchst unwahrscheinlich . Es ist nur eine Menge syntaktischer Zucker für die Weitergabe . Möglicherweise gibt es noch einige andere Verbesserungen an WinForms, die helfen sollen? Dies würde jedoch unter das .NET-Framework fallen und nicht unter C #.
Aaronaught
@Aaronaught Da stimme ich zu, deshalb stelle ich die Frage genau. Ich habe die Frage bearbeitet, um zu verdeutlichen, woher ich komme. Klingt seltsam, dass sie diese Funktion erstellen würden und weiterhin die Verwendung des berüchtigten InvokeRequired-Codestils erfordern.
Alex

Antworten:

17

Ich glaube, Sie werden hier ein paar Dinge durcheinander bringen. Was Sie für Fragen ist bereits möglich , mit System.Threading.Tasksdem asyncund awaitin C # 5 werden nur für die gleiche Funktion einen wenig schöner syntaktischen Zucker zu liefern.

Lassen Sie uns ein Winforms-Beispiel verwenden - fügen Sie eine Schaltfläche und ein Textfeld in das Formular ein und verwenden Sie diesen Code:

private void button1_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew<int>(() => DelayedAdd(5, 10))
        .ContinueWith(t => DelayedAdd(t.Result, 20))
        .ContinueWith(t => DelayedAdd(t.Result, 30))
        .ContinueWith(t => DelayedAdd(t.Result, 50))
        .ContinueWith(t => textBox1.Text = t.Result.ToString(),
            TaskScheduler.FromCurrentSynchronizationContext());
}

private int DelayedAdd(int a, int b)
{
    Thread.Sleep(500);
    return a + b;
}

Führen Sie es aus und Sie werden sehen, dass (a) es den UI-Thread nicht blockiert und (b) Sie nicht die übliche Fehlermeldung "Cross-Thread-Operation ungültig" erhalten - es sei denn, Sie entfernen das TaskSchedulerArgument aus dem letzten ContinueWith, in Welchen Fall wirst du.

Dies ist ein moorhafter Fortführungsstil . Die Magie geschieht in der TaskSchedulerKlasse und speziell in der Instanz, die von abgerufen wird FromCurrentSynchronizationContext. Übergeben Sie dies an eine beliebige Fortsetzung, und geben Sie an, dass die Fortsetzung auf dem Thread ausgeführt werden muss, der die FromCurrentSynchronizationContextMethode aufgerufen hat - in diesem Fall auf dem UI-Thread.

Kellner sind etwas anspruchsvoller in dem Sinne, dass sie wissen, auf welchem ​​Thread sie begonnen haben und auf welchem ​​Thread die Fortsetzung stattfinden muss. So kann der obige Code etwas natürlicher geschrieben werden:

private async void button1_Click(object sender, EventArgs e)
{
    int a = await DelayedAddAsync(5, 10);
    int b = await DelayedAddAsync(a, 20);
    int c = await DelayedAddAsync(b, 30);
    int d = await DelayedAddAsync(c, 50);
    textBox1.Text = d.ToString();
}

private async Task<int> DelayedAddAsync(int a, int b)
{
    Thread.Sleep(500);
    return a + b;
}

Diese beiden sollten sehr ähnlich aussehen, und tatsächlich sind sie sich sehr ähnlich. Die DelayedAddAsyncMethode gibt jetzt ein Task<int>statt eines zurück int, und daher werden awaitnur die Fortsetzungen auf jede dieser Methoden angewendet. Der Hauptunterschied besteht darin, dass der Synchronisierungskontext in jeder Zeile weitergegeben wird, sodass Sie dies nicht explizit wie im letzten Beispiel ausführen müssen.

Theoretisch sind die Unterschiede viel bedeutender. Im zweiten Beispiel button1_Clickwird tatsächlich jede einzelne Zeile in der Methode im UI-Thread ausgeführt, aber die Task selbst ( DelayedAddAsync) wird im Hintergrund ausgeführt. Im ersten Beispiel läuft alles im Hintergrund , mit Ausnahme der Zuweisung, textBox1.Textdie wir explizit dem Synchronisationskontext des UI-Threads zugeordnet haben.

Das ist es, was wirklich interessant ist await- die Tatsache, dass ein Kellner in der Lage ist, auf dieselbe Weise ein- und auszuspringen , ohne Anrufe zu blockieren. Sie rufen await, den aktuellen Thread Nachrichten an die Verarbeitung geht zurück, und wenn es fertig ist, wird die Erwartenden genau abholen, wo sie aufgehört hat , im selben Thread aufhörte es in. Aber in Bezug auf Ihre Invoke/ BeginInvokeKontrast in der Frage, I‘ Es tut mir leid zu sagen, dass Sie das schon vor langer Zeit aufgeben sollten.

Aaronaught
quelle
Das ist sehr interessant @Aaronaught. Ich war mir des Continuation-Passing-Stils bewusst, war mir aber dieser ganzen Sache mit dem "Synchronisationskontext" nicht bewusst. Gibt es ein Dokument, das diesen Synchronisierungskontext mit C # 5 async-await verknüpft? Ich verstehe, dass es sich um eine vorhandene Funktion handelt, aber die Tatsache, dass sie standardmäßig verwendet wird, klingt nach einer großen Sache, vor allem, weil sie erhebliche Auswirkungen auf die Leistung haben muss, oder? Haben Sie noch weitere Kommentare dazu? Danke übrigens für deine Antwort.
Alex
1
@Alex: Um Antworten auf all diese Anschlussfragen zu erhalten, empfehlen wir Ihnen, Async-Leistung: Die Kosten von Async und Warten zu verstehen . Im Abschnitt "Pflege des Kontexts" wird erläutert, wie sich alles auf den Synchronisationskontext bezieht.
Aaronaught
(Synchronisationskontexte sind übrigens nicht neu; sie sind seit 2.0 im Framework enthalten. Die TPL hat ihre Verwendung nur
erheblich
2
Ich frage mich, warum es immer noch viele Diskussionen über die Verwendung des InvokeRequired-Stils gibt und die meisten Threads, die ich gesehen habe, nicht einmal über Synchronisierungskontexte sprechen. Es hätte mir die Zeit gespart, diese Frage zu stellen ...
Alex
2
@Alex: Ich denke, Sie haben nicht an den richtigen Stellen gesucht . Ich weiß nicht, was ich dir sagen soll. Es gibt große Teile der .NET-Community, deren Aufholjagd sehr lange dauert. Zur Hölle, ich sehe immer noch einige Programmierer, die die ArrayListKlasse in neuem Code verwenden. Ich selbst habe noch kaum Erfahrung mit RX. Die Menschen lernen, was sie wissen müssen, und teilen, was sie bereits wissen, auch wenn das, was sie bereits wissen, nicht mehr aktuell ist. Diese Antwort könnte in einigen Jahren veraltet sein.
Aaronaught
4

Ja, für den UI-Thread wird der Rückruf einer Warteoperation für den ursprünglichen Thread des Aufrufers ausgeführt.

Eric Lippert hat vor einem Jahr eine 8-teilige Serie darüber geschrieben: Fabulous Adventures In Coding

EDIT: und hier ist Anders '// build / presentation: channel9

Übrigens, hast du bemerkt, dass wenn du "// build /" auf den Kopf stellst, du "/ plinq //" bekommst?

Nicholas Butler
quelle