Kontynuacja zadania w wątku interfejsu użytkownika

214

Czy istnieje „standardowy” sposób określania, że ​​kontynuacja zadania powinna być uruchamiana w wątku, z którego zostało utworzone zadanie początkowe?

Obecnie mam poniższy kod - działa, ale śledzenie dyspozytora i utworzenie drugiej Akcji wydaje się niepotrzebnym narzutem.

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

Task UITask= task.ContinueWith(() =>
{
    dispatcher.Invoke(new Action(() =>
    {
        this.TextBlock1.Text = "Complete"; 
    }
});
Greg Sansom
źródło
W przypadku twojego przykładu możesz użyć Control.Invoke(Action), tj. TextBlock1.Invokezamiastdispatcher.Invoke
pułkownik Panic
2
Dzięki @ColonelPanic, ale używałem WPF (zgodnie z tagami), a nie WinForm.
Greg Sansom,

Odpowiedzi:

352

Zadzwoń do kontynuacji za pomocą TaskScheduler.FromCurrentSynchronizationContext():

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

Jest to odpowiednie tylko wtedy, gdy bieżący kontekst wykonania znajduje się w wątku interfejsu użytkownika.

Greg Sansom
źródło
39
Jest ważny tylko wtedy, gdy bieżący kontekst wykonania znajduje się w wątku interfejsu użytkownika. Jeśli umieścisz ten kod w innym
zadaniu
3
W .NET 4.5 odpowiedź Johana Larssona powinna być używana jako standardowy sposób kontynuacji zadania w wątku interfejsu użytkownika. Po prostu napisz: czekaj na Task.Run (DoLongRunningWork); this.TextBlock1.Text = "Complete"; Zobacz także: blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
Marcel W
1
Dzięki za uratowanie mi życia. Spędzam godziny, aby wymyślić, jak wywoływać wątek główny w oczekiwaniu / Kontynuuj. Dla wszystkich innych, jak korzysta z Google Firebase SDK dla Unity i nadal ma te same problemy, jest to działające podejście.
CHaP
2
@MarcelW - awaitjest dobrym wzorcem - ale tylko jeśli znajdujesz się w asynckontekście (takim jak zadeklarowana metoda async). Jeśli nie, nadal trzeba zrobić coś takiego.
ToolmakerSteve
33

Dzięki asynchronizacji po prostu wykonujesz:

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);

Jednak:

await Task.Run(() => do some stuff).ConfigureAwait(false);
// continue doing stuff on the same thread as the task finished on.
Johan Larsson
źródło
2
Komentarz pod falsewersją myli mnie. Pomyślałem, że falseoznacza to, że może kontynuować na innym wątku.
ToolmakerSteve
1
@ToolmakerSteve Zależy, o którym wątku myślisz. Wątek roboczy używany przez Task.Run lub wątek wywołujący? Pamiętaj, że „ten sam wątek, na którym zakończono zadanie” oznacza wątek roboczy (unikanie „przełączania” między wątkami). Również ConfigureAwait (true) nie gwarantuje, że kontrola powróci do tego samego wątku , tylko do tego samego kontekstu (chociaż rozróżnienie może nie być znaczące).
Max Barraclough,
@MaxBarraclough - Dzięki, źle odczytałem, który „ten sam wątek” miał na myśli. unikanie przełączania się między wątkami w sensie maksymalizacji wydajności poprzez użycie dowolnego wątku, który się uruchomił [aby wykonać zadanie „zrób coś”], co dla mnie to wyjaśnia.
ToolmakerSteve,
1
Pytanie nie określa bycia wewnątrz asyncmetody (której użycie jest konieczne await). Jaka jest odpowiedź, kiedy awaitnie jest dostępna?
ToolmakerSteve
22

Jeśli masz wartość zwracaną, którą musisz wysłać do interfejsu użytkownika, możesz użyć wersji ogólnej takiej jak ta:

W moim przypadku jest to wywoływane z MVVM ViewModel.

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
źródło
Zgaduję, że = zanim GenerateManifest to literówka.
Sebastien F.,
Tak - już nie ma! Dzięki.
Simon_Weaver,
11

Chciałem tylko dodać tę wersję, ponieważ jest to tak przydatny wątek i myślę, że jest to bardzo prosta implementacja. Użyłem tego wiele razy w różnych typach, jeśli aplikacja wielowątkowa:

 Task.Factory.StartNew(() =>
      {
        DoLongRunningWork();
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() =>
              { txt.Text = "Complete"; }));
      });
Dziekan
źródło
2
Nie przegrywanie, ponieważ w niektórych scenariuszach jest to realne rozwiązanie; jednak zaakceptowana odpowiedź jest znacznie lepsza. Jest niezależny od technologii ( TaskSchedulerjest częścią BCL, Dispatchernie jest) i może być używany do komponowania złożonych łańcuchów zadań, ponieważ nie trzeba się martwić o operacje asynchroniczne typu „odpal i zapomnij” (np. BeginInvoke).
Kirill Shlenskiy
@Kirill można trochę rozwinąć, ponieważ niektóre wątki SO jednogłośnie zadeklarowały, że dyspozytor jest poprawną metodą, jeśli używa się WPF WinForm: Można wywołać aktualizację GUI asynchronicznie (używając BeginInvoke) lub synchronicznie (Invoke), choć zwykle asynchronicznie jest używany, ponieważ nie chcemy blokować wątku w tle tylko dla aktualizacji GUI. Czy FromCurrentSynchronizationContext nie umieszcza zadania kontynuacji w kolejce komunikatów głównego wątku w taki sam sposób, jak dyspozytor?
Dziekan
1
Zgadza się, ale OP z pewnością pyta o WPF (i oznaczył to tagiem) i nie chce zachować odwołania do żadnego programu rozsyłającego (i zakładam również dowolny kontekst synchronizacji - możesz to uzyskać tylko z głównego wątku i musisz zapisz gdzieś odniesienie). Właśnie dlatego podoba mi się opublikowane przeze mnie rozwiązanie: jest wbudowane bezpieczne wątek, które nie wymaga tego. Myślę, że jest to niezwykle przydatne w kontekście WPF.
Dziekan
3
Chciałem tylko wzmocnić mój ostatni komentarz: programista nie tylko musi przechowywać kontekst synchronizacji, ale musi wiedzieć, że jest on dostępny tylko z głównego wątku; ten problem stał się przyczyną nieporozumień w dziesiątkach SO pytań: Ludzie cały czas starają się uzyskać to z wątku roboczego. Jeśli sam kod został przeniesiony do wątku roboczego, nie działa z powodu tego problemu. Ze względu na rozpowszechnienie WPF należy to wyjaśnić tutaj w tym popularnym pytaniu.
Dziekan
1
... Niemniej jednak należy zwrócić uwagę na obserwację Deana dotyczącą [akceptowanej odpowiedzi] konieczności śledzenia kontekstu synchronizacji, jeśli kod może nie znajdować się w głównym wątku, a unikanie tego jest zaletą tej odpowiedzi.
ToolmakerSteve
1

Dostałem się tutaj przez Google, ponieważ szukałem dobrego sposobu na zrobienie czegoś w wątku interfejsu użytkownika po wejściu do wywołania Task.Run - używając następującego kodu, którego możesz użyć, awaitaby wrócić do wątku interfejsu użytkownika.

Mam nadzieję, że to komuś pomoże.

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;
    }
}

Stosowanie:

... code which is executed on the background thread...
await UI.Thread;
... code which will be run in the application dispatcher (ui thread) ...
Dbl
źródło
Bardzo mądry! Jednak dość nieintuicyjna. Sugeruję zrobienie staticklasy UI.
Theodor Zoulias