Fortsetzung der Aufgabe im UI-Thread

214

Gibt es eine "Standard" -Methode, um anzugeben, dass eine Aufgabenfortsetzung auf dem Thread ausgeführt werden soll, aus dem die ursprüngliche Aufgabe erstellt wurde?

Derzeit habe ich den folgenden Code - er funktioniert, aber den Dispatcher im Auge zu behalten und eine zweite Aktion zu erstellen, scheint unnötiger Aufwand zu sein.

dispatcher = Dispatcher.CurrentDispatcher;
Task task = Task.Factory.StartNew(() =>
{
    DoLongRunningWork();
});

Task UITask= task.ContinueWith(() =>
{
    dispatcher.Invoke(new Action(() =>
    {
        this.TextBlock1.Text = "Complete"; 
    }
});
Greg Sansom
quelle
Im Fall Ihres Beispiels könnten Sie verwenden Control.Invoke(Action), dh. TextBlock1.Invokeeher alsdispatcher.Invoke
Colonel Panic
2
Danke @ColonelPanic, aber ich habe WPF (wie markiert) verwendet, keine Winforms.
Greg Sansom

Antworten:

352

Nennen Sie die Fortsetzung mit TaskScheduler.FromCurrentSynchronizationContext():

    Task UITask= task.ContinueWith(() =>
    {
     this.TextBlock1.Text = "Complete"; 
    }, TaskScheduler.FromCurrentSynchronizationContext());

Dies ist nur geeignet, wenn sich der aktuelle Ausführungskontext im UI-Thread befindet.

Greg Sansom
quelle
39
Es ist nur gültig, wenn sich der aktuelle Ausführungskontext im UI-Thread befindet. Wenn Sie diesen Code in eine andere Aufgabe einfügen, erhalten Sie InvalidOperationException (siehe Abschnitt Ausnahmen )
stukselbax
3
In .NET 4.5 sollte die Antwort von Johan Larsson als Standardmethode für die Fortsetzung einer Aufgabe im UI-Thread verwendet werden. Schreiben Sie einfach: Warten Sie auf Task.Run (DoLongRunningWork); this.TextBlock1.Text = "Vollständig"; Siehe auch: blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
Marcel W
1
Danke, dass du mein Leben gerettet hast. Ich verbringe Stunden damit, herauszufinden, wie man Hauptthread-Dinge innerhalb von await / ContinueWith aufruft. Für alle anderen, die Google Firebase SDK für Unity verwenden und dennoch dieselben Probleme haben, ist dies ein funktionierender Ansatz.
CHaP
2
@MarcelW - awaitist ein gutes Muster - aber nur, wenn Sie sich in einem asyncKontext befinden (z. B. einer deklarierten Methode async). Wenn nicht, ist es immer noch notwendig, so etwas wie diese Antwort zu tun.
ToolmakerSteve
33

Mit Async machen Sie einfach:

await Task.Run(() => do some stuff);
// continue doing stuff on the same context as before.
// while it is the default it is nice to be explicit about it with:
await Task.Run(() => do some stuff).ConfigureAwait(true);

Jedoch:

await Task.Run(() => do some stuff).ConfigureAwait(false);
// continue doing stuff on the same thread as the task finished on.
Johan Larsson
quelle
2
Der Kommentar unter der falseVersion verwirrt mich. Ich dachte false, es könnte in einem anderen Thread weitergehen.
ToolmakerSteve
1
@ToolmakerSteve Hängt davon ab, an welchen Thread Sie denken. Der von Task.Run verwendete Arbeitsthread oder der Aufruferthread? Denken Sie daran, dass "derselbe Thread, für den die Aufgabe beendet wurde" den Arbeitsthread bedeutet (das "Wechseln" zwischen Threads wird vermieden). Außerdem garantiert ConfigureAwait (true) nicht, dass das Steuerelement zum selben Thread zurückkehrt , nur zum selben Kontext (obwohl die Unterscheidung möglicherweise nicht signifikant ist).
Max Barraclough
@ MaxBarraclough - Danke, ich habe falsch verstanden, welcher "gleiche Thread" gemeint war. Vermeiden Sie das Wechseln zwischen Threads im Sinne einer Maximierung der Leistung, indem Sie einen beliebigen Thread verwenden, der gerade ausgeführt wird [um die Aufgabe "Einige Dinge erledigen" auszuführen]. Dies verdeutlicht dies für mich.
ToolmakerSteve
1
Die Frage gibt nicht an, innerhalb einer asyncMethode zu sein (was für die Verwendung erforderlich ist await). Was ist die Antwort, wenn sie awaitnicht verfügbar ist?
ToolmakerSteve
22

Wenn Sie einen Rückgabewert haben, den Sie an die Benutzeroberfläche senden müssen, können Sie die generische Version wie folgt verwenden:

Dies wird in meinem Fall von einem MVVM ViewModel aufgerufen.

var updateManifest = Task<ShippingManifest>.Run(() =>
    {
        Thread.Sleep(5000);  // prove it's really working!

        // GenerateManifest calls service and returns 'ShippingManifest' object 
        return GenerateManifest();  
    })

    .ContinueWith(manifest =>
    {
        // MVVM property
        this.ShippingManifest = manifest.Result;

        // or if you are not using MVVM...
        // txtShippingManifest.Text = manifest.Result.ToString();    

        System.Diagnostics.Debug.WriteLine("UI manifest updated - " + DateTime.Now);

    }, TaskScheduler.FromCurrentSynchronizationContext());
Simon_Weaver
quelle
Ich vermute, dass das = vor GenerateManifest ein Tippfehler ist.
Sebastien F.
Ja - jetzt weg! Vielen Dank.
Simon_Weaver
11

Ich wollte nur diese Version hinzufügen, weil dies ein so nützlicher Thread ist und ich denke, dass dies eine sehr einfache Implementierung ist. Ich habe dies mehrfach in verschiedenen Arten von Multithread-Anwendungen verwendet:

 Task.Factory.StartNew(() =>
      {
        DoLongRunningWork();
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() =>
              { txt.Text = "Complete"; }));
      });
Dean
quelle
2
Kein Downvoting, da dies in einigen Szenarien eine praktikable Lösung ist. Die akzeptierte Antwort ist jedoch viel besser. Es ist technologieunabhängig ( TaskSchedulerist Teil von BCL, Dispatcherist es nicht) und kann zum Erstellen komplexer Aufgabenketten verwendet werden, da keine asynchronen Operationen zum Feuern und Vergessen (wie z. B. BeginInvoke) erforderlich sind .
Kirill Shlenskiy
@Kirill können Sie ein wenig erweitern, da einige SO-Threads den Dispatcher einstimmig als die richtige Methode deklariert haben, wenn WPF von WinForms verwendet wird: Sie können ein GUI-Update entweder asynchron (mit BeginInvoke) oder synchron (Invoke) aufrufen, obwohl dies normalerweise asynchron ist wird verwendet, weil man einen Hintergrund-Thread nicht nur für ein GUI-Update blockieren möchte. Setzt FromCurrentSynchronizationContext die Fortsetzungsaufgabe nicht genauso wie der Dispatcher in die Haupt-Thread-Nachrichtenwarteschlange?
Dean
1
Richtig, aber das OP fragt sicherlich nach WPF (und hat es so markiert) und möchte keinen Verweis auf einen Dispatcher behalten (und ich gehe auch von einem Synchronisationskontext aus - Sie können dies nur vom Hauptthread erhalten und müssen es irgendwo einen Verweis darauf aufbewahren). Aus diesem Grund gefällt mir die von mir veröffentlichte Lösung: Es ist eine thread-sichere statische Referenz eingebaut, die nichts davon erfordert. Ich denke, dass dies im WPF-Kontext äußerst nützlich ist.
Dean
3
Ich wollte nur meinen letzten Kommentar verstärken: Der Entwickler muss nicht nur den Synchronisierungskontext speichern, sondern auch wissen, dass dieser nur über den Hauptthread verfügbar ist. Dieses Problem hat bei Dutzenden von SO-Fragen zu Verwirrung geführt: Die Leute versuchen ständig, dies aus dem Worker-Thread herauszuholen. Wenn ihr Code selbst in einen Arbeitsthread verschoben wurde, schlägt dies aufgrund dieses Problems fehl. Aufgrund der Verbreitung von WPF sollte dies hier in dieser populären Frage unbedingt geklärt werden.
Dean
1
... dennoch ist es wichtig zu beachten, dass Dean beobachtet, dass [die akzeptierte Antwort] den Synchronisierungskontext verfolgen muss, wenn sich der Code möglicherweise nicht im Hauptthread befindet, und dass dies ein Vorteil dieser Antwort ist.
ToolmakerSteve
1

Ich bin über Google hierher gekommen, weil ich nach einer guten Möglichkeit gesucht habe, Dinge awaitim UI-Thread zu tun, nachdem ich mich in einem Task.Run-Aufruf befunden habe. Mit dem folgenden Code können Sie wieder zum UI-Thread zurückkehren.

Ich hoffe das hilft jemandem.

public static class UI
{
    public static DispatcherAwaiter Thread => new DispatcherAwaiter();
}

public struct DispatcherAwaiter : INotifyCompletion
{
    public bool IsCompleted => Application.Current.Dispatcher.CheckAccess();

    public void OnCompleted(Action continuation) => Application.Current.Dispatcher.Invoke(continuation);

    public void GetResult() { }

    public DispatcherAwaiter GetAwaiter()
    {
        return this;
    }
}

Verwendung:

... code which is executed on the background thread...
await UI.Thread;
... code which will be run in the application dispatcher (ui thread) ...
Dbl
quelle
Sehr schlau! Ziemlich unintuitiv. Ich schlage vor, staticdie Klasse zu machen UI.
Theodor Zoulias