Laden...

Asynchrone Ausführung in WPF: Task.Run() oder Dispatcher.InvokeAsync?

Erstellt von sugar76 vor 5 Jahren Letzter Beitrag vor 5 Jahren 3.025 Views
S
sugar76 Themenstarter:in
69 Beiträge seit 2017
vor 5 Jahren
Asynchrone Ausführung in WPF: Task.Run() oder Dispatcher.InvokeAsync?

Hallo allerseits,

in einem ViewModel möchte ich die Möglichkeit haben, bestimmte Eigenschaften asynchron zu aktualisieren.

Hier ein Beispiel eines ViewModels für ein DataGrid (der Code ist aufs Wesentliche reduziert). Die Methode RefreshAsync aktualisiert die Liste der Elemente, welche mittels Binding im DataGrid angezeigt werden.

namespace MyApp.ViewModel
{
    public abstract class ListViewModelBase<T> : ViewModelBase
    {
        public RangeEnabledObservableCollection<T> ItemList { get; protected set; }

        public ListViewModelBase()
        {
             ItemList = new RangeEnabledObservableCollection<T>();
        }

        public async virtual Task RefreshAsync()
        {
            IEnumerable<T> items = await Task.Run(() => GetItems());
            ItemList.Replace(items);
        }

        public abstract IEnumerable<T> GetItems();
    }
}

Jetzt frage ich mich: ich könnte die Zeile

IEnumerable<T> items = await Task.Run(() => GetItems());

durch diese ersetzen:

IEnumerable<T> items = await Application.Current.Dispatcher.InvokeAsync(() => GetItems());

Meine Frage: wäre es hier besser, statt Task.Run() den Dispatcher-Thread zu verwenden, da ja hier letztlich die GUI aktualisiert wird?

Gruß

6.911 Beiträge seit 2009
vor 5 Jahren

Hallo sugar76,

kommt darauf an wie aufwändig GetItems ist. Ist es nur wenig aufwändig, so kannst du den Dispatcher dafür verwenden. Ist es jedoch aufwändiger -- z.B. dauert länger als 1s --, so würde ich die Arbeit lieber im ThreadPool erledigen lassen, da so der Dispatcher für UI-Aufgaben (rendern, ...) frei bleibt.

Wenn GetItems auf eine Datenbank od. einen WebService zugreift, so kannst du dort das async "durchziehen", dann wird der ThreadPool auch nicht wirklich belastet. Achte bei diesen Aufrufen aber dass ConfigureAwait(false) angegeben wird, denn hier ist es nicht nötig zurück in den UI-Thread zu delegieren. Das ist nur beim letzten await nötig, von dem aus es dann zur UI geht (wie in deinem Code).

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

S
sugar76 Themenstarter:in
69 Beiträge seit 2017
vor 5 Jahren

Super, besten Dank für die Erklärung. Der Dispatcher sollte also nur für Aufgaben verwendet werden, welche unmittelbar die GUI betreffen und nicht für sonstige Aufgaben (Daten aus der Datenbank laden, etc.).

Da GetItems() abstrakt und somit die Dauer unbekannt ist, werde ich weiterhin das Task.Run() verwenden.

Gruß

W
955 Beiträge seit 2010
vor 5 Jahren

gfoidl meint wenn die aufzurufende Klasse eine asynchrone Schnittstelle aufweist kannst du diese mit await konsumieren und musst nicht Task.Run verwenden.

E
1 Beiträge seit 2018
vor 5 Jahren

Der Dispatcher sollte also nur für Aufgaben verwendet werden, welche unmittelbar die GUI betreffen und nicht für sonstige Aufgaben

S
sugar76 Themenstarter:in
69 Beiträge seit 2017
vor 5 Jahren

gfoidl meint wenn die aufzurufende Klasse eine asynchrone Schnittstelle aufweist kannst du diese mit await konsumieren und musst nicht Task.Run verwenden.

Dann müsste man aber GetItems selbst als asynchron definieren oder?

public abstract Task<IEnumerable<T>> GetItemsAsync();
2.079 Beiträge seit 2012
vor 5 Jahren

Sei vorsichtig mit diesen beiden Aufrufen.

Dispatcher.InvokeAsync führt Aufgaben im UI-Thread durch.
Task.Run führt Aufgaben im Thread vom Thread-Pool aus, nicht im UI-Thread.

Eine Aufgabe, die im UI-Thread ausgeführt wird, blockiert Diesen so lange, bis die Aufgabe abgeschlossen ist.
Wenn eine Aufgabe, die in einem anderen Thread ausgeführt wird, auf Daten zugreift, die von einem anderen Thread gelesen oder verändert, kann das zu Problemen führen. Wenn eine Änderung in so einem Thread dazu führt, dass WPF-Controls aktualisiert werden, wird das definitiv zu einem Fehler führen.

Soll heißen:
Sehr aufwändige, langandauernde Aufgaben gehören in einen eigenen Thread, Du solltest aber darauf achten, dass die Zugriffe synchronisiert werden.
Änderungen, welche die UI aktualisieren, müssen also wieder in den UI-Thread synchronisiert werden, dafür ist dann Dispatcher.InvokeAsync geeignet.

Besser ist aber, dass Du Dispatcher.InvokeAsync gar nicht brauchst.
Wenn Du z.B. über einen Button die Methode aufrufen lässt, wird diese Methode im UI-Thread ausgeführt. Wenn Du einen Task hast, dann kannst Du dich darauf verlassen, dass der Code nach dem await wieder im UI-Thread ausgeführt wird. Du musst also nur darauf achten, dass Du keine Aufgaben an Task.Run übergibst, die in einem extra Thread laufen können.

Oder in kurz:
Dein Code in RefreshAsync ist mit Task.Run, genau so wie er ist, richtig, wenn GetItems nicht die UI aktualisiert und auch anderweitig in einem eigenen Thread laufen darf.
Geht das nicht und Du hast darin z.B. Datenbank-Aufrufe (z.B. wenn das ORM Async unterstützt), dann solltest Du keinen neuen Task starten, Du solltest stattdessen die Datenbank-AUfrufe awaiten, der Rest wird dann wieder im UI-Thread ausgeführt.

public async Task RefreshAsync()
{
    IEnumerable<T> items = await GetItemsAsync();
    ItemList.Replace(items);
}

ublic async Task<IEnumerable<T>> GetItemsAsync()
{
    // Do something on UI-Thread
    var data = await Connection.GetDataAsync();
    // Do something on UI-Thread
    var readItems = await ReadDataAsync(data);
    // Do something on UI-Thread
    return readItems;
}
6.911 Beiträge seit 2009
vor 5 Jahren

Hallo sugar76,

Dann müsste man aber GetItems selbst als asynchron definieren oder?

Ja.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

S
sugar76 Themenstarter:in
69 Beiträge seit 2017
vor 5 Jahren

... und wieder was gelernt 😁