Laden...

Wie realisiere ich die korrekte Fehlerbehandlung bei Tasks?

Erstellt von tobi45f vor 4 Jahren Letzter Beitrag vor 4 Jahren 3.047 Views
T
tobi45f Themenstarter:in
59 Beiträge seit 2017
vor 4 Jahren
Wie realisiere ich die korrekte Fehlerbehandlung bei Tasks?

Hallo zusammen,

ich habe in meinem Code vermutlich einen Fehler gemacht, zumindest verstehe ich nicht, warum der Code so abläuft. Vielleicht kann mir jemand erklären, woran das liegt.
Und zwar habe ich zwei Klassen:
MainWindow, für die GUI Elemente und
Berechnung, wo der auszuführende Code drin steckt.
Wenn ich einen Button in der GUI drücke, dann wird ein neuer Task in der Berechnungsklasse gestartet. Wenn das Senden einen Fehler hat, dann läuft die Fehlerbehandlung leider aus dem Ruder und ich verstehe nicht so ganz, warum er so wirr hin und her springt.

Hier das Event, wenn der Button zum senden gedrückt wird in meinem MainWindow:


private async void Btn_FidListe_Click(object sender, RoutedEventArgs e)
        {
            tokenSource = new CancellationTokenSource();
            var token = tokenSource.Token;
            try
            {
                await Task.Factory.StartNew(() => berechnung.PostTAsync(pathFidList, berechnung.GetMarketArea()), token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
                // Das erste Element in PostTAsync ist die URI und das zweite Element ist das zu übergebene Objekt
                
            }
            catch (TaskCanceledException etask)
            {
                tb_ausgabe.Text += (etask.Message) + "\n";
            }
            catch (OperationCanceledException)
            {
                tb_ausgabe.Text += "Übermittlung wurde abgebrochen\n";
            }
            catch (AuktionAlreadyExistsException eAutkion)
            {
                tb_ausgabe.Text += (eAutkion.Message) + "\n";
            }            
            catch (HttpRequestException eHTTP)
            {
                tb_ausgabe.Text += (eHTTP.Message) + "\n";
            }
            catch
            {
                tb_ausgabe.Text += "sonstiger Fehler\n";
            }
            finally
            {
                tokenSource.Dispose();
            }
        }

Hier meine Methode in der Berechnungsklasse


public async Task<bool> PostTAsync<T>(string path, T objekt)
        {
            HttpClient client = new HttpClient
            {
                BaseAddress = new Uri(path)
            };
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            var byteArray = Encoding.ASCII.GetBytes("x:x");
            client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));

            try
            {
                System.Net.WebRequest.DefaultWebProxy.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
                HttpResponseMessage response = await client.PostAsJsonAsync<T>("", objekt); // Bei dieser Anweisung hat er Probleme, wenn der Server nicht erreichbar ist. Er wartet nicht und springt zurück

                response.EnsureSuccessStatusCode();

                if ((int)response.StatusCode == 450)
                {
                    //Nachfrage schon vorhanden
                    throw new AuktionAlreadyExistsException(response.Content.ReadAsStringAsync().Result);
                }
                else response.EnsureSuccessStatusCode();


                return response.IsSuccessStatusCode;
            }
            catch (Exception e)
            {
                Console.WriteLine("Post auf " + path + " Failed \nDer Server konnte nicht erreicht werden.\n" + e.Message);
                return false;
            }
        }

Also was macht der Code:

Ist der Server aber nicht erreichbar, so kommt er in PostTAsync bis "HttpResponseMessage response = await client.PostAsJsonAsync<T>", springt dann direkt aus der Methode raus und geht in den finally Block des Aufrufers in Btn_FidListe_Click, beendet das Click-Event, merkt aber dann, dass das posten (HttpResponseMessage response = await client.PostAsJsonAsync<T>) fehlgeschlagen ist, löst die Exception in PostTAsync<T> aus und beendet die Methode.
Evtl. auftretende Exceptions können also vom Aufrufer gar nicht mehr abgearbeitet werden, da der Try-Catch Block ja schon beendet wurde. Aus einem mir nicht verständlichen Grund wird nicht auf die Await-Anweisung (await client.PostAsJsonAsync<T>) gewartet. Verstehe ich hier den Await-Code falsch oder wo liegt mein Denkfehler?

Der Try-Catch Block - Ich habe die Fehlerbehandlung im Aufrufer/MainWindow, um auftretende Fehler dort zu behandeln. Wenn ich mir das nicht falsch beigebracht habe, dann sollte das korrekt sein, alle auftretenden Fehler dort zu behandeln? Warum habe ich also einen try-catch Block in der Berechnungsklasse? Wenn ich diesen dort nicht habe, dann erkennt er leider den fehlgeschlagenen Post gar nicht! Dass das so mit der AuktionAlreadyExistsException Käse ist, ist mir bewusst. Leider kommt diese nicht im Aufrufer an.

Wie mache ich das mit den Try-Catch Blöcken korrekt und wie bekomme ich das hin, dass er auf die Await Anweisung tatsächlich wartet, so wie es in der Dokumentation steht?
Liegt es daran, wie ich denk Task aufrufe? Da gibts ja zig Varianten.

Mag mir jemand auf die Sprünge helfen? 😃
Gruß Tobias

6.911 Beiträge seit 2009
vor 4 Jahren

Hallo tobi45f,

ersetzte


await Task.Factory.StartNew(() => berechnung.PostTAsync(pathFidList, berechnung.GetMarketArea()), token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

durch


await berechnung.PostTAsync(pathFidList, berechnung.GetMarketArea()), token);

(den CancellationToken sollte als Argument an PostTAsync übergeben werden können)

Dann sollte das Fehlerhandling korrekt klappen.
Bei deinem Code mit Task.Factory.StartNew wird nicht nur unnötig ein Thread gestartet, sondern auch await "erwartet" den Task der gerade gestartet wurde und nicht jenen der vom PostAsJsonAsync stammt. Somit resultiert das in einen "unbehandleten Task-Fehler" und wegen dem finally wird dort fortgesetzt.

Ohne das Task.Factory.StartNew wird bei await das "Ergebnis" von PostAsJsonAsync erwartet und das kann ein Resultat (hier das bool) od. eben ein Fehler sein.

Da PostAsJsonAsync für sich schon IO-asynchron ist, macht es -- wie vorhin schon kurz erwähnt -- keinen Sinn extra einen Thread zu starten (wegen LongRunning wird kein ThreadPool-Thread verwendet).

Weiters solltest du den HttpClient (für die gleiche Url) nicht jedesmal neu erstellen, da selbst beim Dispose die Socket-Verbindungen nicht ganz geschlossen werden und es somit zu einer "Ausschöpfung" kommen kann. Google mal danach, dieser Punkt wird ausführlich behandelt, daher gebe ich hier nicht alles wieder.
Kurz: verwende am besten die HttpClientFactory, denn diese verwendet einen Pool und erledigt das alles für dich.

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!"

T
tobi45f Themenstarter:in
59 Beiträge seit 2017
vor 4 Jahren

Hi Gü,

danke für deine schnelle Antwort und die Erklärung (bei der ich gleich noch mal nachfragen muss🙂)! jetzt funktioniert es prima und ich kann mit den Try-Catch Block beim posten sparen, da er beim Fehler zum Aufruf springt! Prima 🙂

Im geposteten Code habe ich ja nur die Methode, in der das json Objekt gepostet wird. Vorher hatte ich in der Methode noch eine Berechnung - zur Erstellung des Objekts - mit drin. Diese Methode habe ich jetzt ausgelagert. Als alles zusammen war, hatte ich die cancellationToken verwendet. Beim entzerren habe ich das wohl beim Aufruf nicht ausgebessert (kam ja auch kein Fehler 😄)

Grob kann ich den Fehler verstehen, auch warum er so hin und her springt mit den Tasks, was er erwartet etc.

Wieso muss ich da keinen Task starten? So richtig verstehe ich das dann wohl nicht, wann ich über einen neuen Task die Methode aufrufe und wann ich nur die asynchrone Methode mit await aufrufe? Wonach kann man sich da richten, wie/wann man was anwendet?
Beispielsweise die Routine, die dem Post vorweg geht. Diese hat eine lange Berechnungsdauer (einige Minuten). Wie würde ich diese aufrufen, mit oder ohne Task.Factory.StartNew? Wenn ich zumindest über "await berechnung.Prozedur()" ohne Task.Factory.StartNew aufrufe, dann friert mein Fenster ein, aber die Fehlerbehandlung ist korrekt. Mache ich das wie zuvor, ist meine Fehlerbehandlung doof aber mein Fenster friert nicht ein 🤔 🤔

Bzgl. des httpClient meinst du das hier, zwar nicht HttpClientFactory aber inhaltlich sehr ähnlich? Das hatte ich heute schon beim Recherchieren gefunden.

Gruß Tobias

6.911 Beiträge seit 2009
vor 4 Jahren

Hallo tobi45f,

wann ich über einen neuen Task die Methode aufrufe und wann ich nur die asynchrone Methode mit await aufrufe? Wonach kann man sich da richten, wie/wann man was anwendet?

Grundsätzlich muss bei Vorgängen unterschieden werden ob diese CPU- od. IO-lastig sind.
CPU-lastig sind z.B. Berechnungen die direkt im RAM erfolgen können.
IO ist Datei-, Netzwerk-, Datenbank-Zugriff usw.

Task ist dabei eine .NET-Abstraktion von "Arbeitsaufträgen", welche nicht zwingend mit Thread gleichzusetzen sind.

Der Großteil von IO kann in .NET asynchron erfolgen und wird (mittlerweile) durch Tasks abstrahiert. Z.B.


public async Task<int> DownloadAsync(string url)
{
    // Code vorher
    string result = await _httpClient.GetStringAsync(url);
    // Code nachher
    return result.Length;
}

Was passiert hier?1.die Methode DownloadAsync wird synchron aufgerufen 1.Code vorher wird synchron ausgeführt 1.Code GetStringAsync wird im HttpClient synchron ausgeführt, bis intern ein IO-async-Vorgang gestartet wird (der HTTP-Request), dann wird von GetStringAsync ein Task<string> zurückgegeben 1.durch das await (und dem vom C#-Compiler umgeschrieben Code) wird das Ergebnis vom Task<string> erwartet (nota bene: ich schreibe bewusst nicht "gewartet", denn diese würde blockierendes Warten implizieren und das gilt es nämlich zu vermeiden, daher "erwartet") 1.DownloadAsync gibt dem Aufrufer der Methode ein Task<int>-Objekt zurück 1.sobald der Http-Request fertig ist und der Http-Response zurückkommt, so wird vom IO-ThreadPool ein sogenannter "IO-Abschlussthread" genommen, welcher die weitere Verarbeitung durchfüht --> jetzt wird mit dem Code nach dem await (der Continuation) forgefahren 1.Code nachher wird wieder syncrhon ausgeführt (aber möglicherweise in einem anderen Therad als Code vorher*) 1.es wird das Ergebnis return result.Length zurückgeben, dabei der Task<int> "vervollständigt" und dessen Result-Eigenschaft hat nun den Wert der per return zurückgegeben wird od. die Exception-Eigenschaft hat einen Fehler im Fehlerfall

* dieses Verhalten kann auch mit ConfigureAwait beeinflusst werden -- behandle ich hier nicht weiter

Der Vorteil von async-IO ist, dass keine CPU-Threads v.a. aus dem ThreadPool verbraten werden od. keine solche Threads unnötig durch Warten blockiert werden -- dies ist v.a. in Server-Umgebungen für die Skalierbarkeit entscheidend.

Daher sollte IO so gut es möglich ist asynchron erfolgen.
In ASP.NET Core sind standardmäßig z.B. keine synchronen (= blockierenden) IO-Vorgänge gestattet (zu Recht 😉).
Aber auch für Dektop-Anwendungen ist dies vorteilhaft, da das gesamte System einfach besser läuft, wenn nicht (wertvolle) CPU-Thread wartend ihre Zeit verbringen.

Bei reiner CPU-Last (Berechnungen, etc.) ist zwischen Server- und Desktop-Anwendungen zu unterscheiden.

Server-Anwendungen können die Berechnungen durchwegs in ihrem Request-Thread durchführen (nicht jedoch IO-Vorgänge, siehe vorhin). Da der Request-Thread bereits dem ThreadPool entnommen wurde, macht es kaum Sinn nochmals in einen anderen ThreadPool-Thread die Arbeit zu delegieren.

Desktop-Anwendungen müssen wegen [FAQ] Warum blockiert mein GUI? CPU-lastige Aufgaben in einem nicht GUI-Thread ausführen, d.h. die Arbeit in einem anderen Thread auslagern.
Wenn es sich um "kurze" Dauern für die CPU-Aufgabe handelt ist der ThreadPool zu bevorzugen, da das Erstellen und Zerstören eines Threads recht aufwändig ist (daher gibt es ja den ThreadPool). Dauer die CPU-Aufgabe zu lange, so wird zwar der ThreadPool durch seine Thread-Injektion-Heuristik mehr Thread dem Pool hinzufügen, aber optimal arbeitet der ThreadPool nur für kurze Aufgaben.
Kurz und lang sind dabei relativ, aber so als groben Richtwert kann man 1s verwenden.
Der Einfachheithalber würde ich aber für Berechnungen auch den ThreadPool verwenden, da der Code einfach kürzer und eleganter wird.

Also um die oben zitierte Frage auch kurz zu beantworten:* für IO "immer" per async / await ohne Task.Factory.StartNew bzw. ohne Task.Run

  • für CPU bei Desktop mittels Task.Run (od. entsprechende ThreadPool.QueueUserWorkItem-Äquivalente)

die Routine, die dem Post vorweg geht. Diese hat eine lange Berechnungsdauer (einige Minuten)

OK, das wusste ich vorher noch nicht. Es geht dabei um GetMarketArea? Ist das wirklich eine Berechnung im Sinne von CPU-Last od. doch eher Datenbank-Zugriffe, usw. also IO?

Nehmen wir an es ist CPU, dann ist auch das möglich. Beispiel:


#define CPU_BOUND

using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        private readonly Worker _worker = new Worker();
        private CancellationTokenSource _cancellationTokenSource;

        public MainWindow()
        {
            InitializeComponent();
        }

        private async void StartButton_Click(object sender, RoutedEventArgs e)
        {
            if (_cancellationTokenSource == null)
                _cancellationTokenSource = new CancellationTokenSource();

            startButton.IsEnabled = false;
            stopButton.IsEnabled = true;

            try
            {
                statusTextBox.Text = "MarketArea...";
#if CPU_BOUND
                MarketArea marketArea = await Task.Run(() => _worker.ComputeMarketArea(), _cancellationTokenSource.Token);
                if (_cancellationTokenSource?.IsCancellationRequested ?? true) return;
#else
                MarketArea marketArea = await _worker.GetMarektAreaAsync(_cancellationTokenSource.Token);
#endif
                statusTextBox.Text = "MarketArea OK";

                if (await _worker.PostObjectAsync("bla bla", marketArea, _cancellationTokenSource.Token))
                {
                    statusTextBox.Text = "OK";
                }
                else
                {
                    // Nicht Erfolg behandeln (od. PostObjectAsync gibt einen string / Exception zurück, dazu müsste ich
                    // aber das Problem / Aufgabe besser verstehen)
                }
            }
            catch (OperationCanceledException)
            {
                statusTextBox.Text = "Übermittlung wurde abgebrochen";
            }
            catch (Exception ex)
            {
                statusTextBox.Text = ex.Message;
            }
            finally
            {
                this.Reset();
            }
        }

        private void StopButton_Click(object sender, RoutedEventArgs e) => this.Reset();

        private void Reset()
        {
            _cancellationTokenSource?.Dispose();
            _cancellationTokenSource = null;

            this.ResetButtons();
            statusTextBox.Text = string.Empty;
        }

        private void ResetButtons()
        {
            startButton.IsEnabled = true;
            stopButton.IsEnabled = false;
        }
    }

    public class MarketArea { }

    public class Worker
    {
        private readonly HttpClient _httpClient = new HttpClient();

        public async Task<MarketArea> GetMarektAreaAsync(CancellationToken cancellationToken = default)
        {
            // Lange Operation simulieren
            await Task.Delay(500, cancellationToken).ConfigureAwait(false);

            return new MarketArea();
        }

        public MarketArea ComputeMarketArea()
        {
            // Lange Berechnung simulieren
            for (int i = 0; i < int.MaxValue; ++i)
            {
            }

            return new MarketArea();
        }

        public async Task<bool> PostObjectAsync<T>(string path, T data, CancellationToken cancellationToken = default)
        {
            // HttpClient-header bereits gesetzt

            try
            {
                // Wenn hier oft der Server nicht verfügbar ist, so kann ein Retry, etc. eingebaut werden
                // Siehe z.B. Polly (einfach googlen im Kontext)
                HttpResponseMessage response = await _httpClient.PostAsJsonAsync(path, data, cancellationToken).ConfigureAwait(false);

                if ((int)response.StatusCode == 450)
                {
                    // Logging -- kein Grund eine Exception zu schmeißen, wenn diese ein paar Zeilen später im catch
                    // gefangen wird, das kann dann gleich hier erfolgen
                    string msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
                    // msg verarbeiten

                    return false;
                }

                return response.IsSuccessStatusCode;
            }
            catch (Exception ex)
            {
                // Logging, etc. von ex

                // Es geht hier nur um den Fehlerfall, daher den Fehler erneut schmeißen
                // Nicht HTTP-200 werden hier nicht behandelt
                throw;
            }
        }
    }
}

Es gibt so viele Möglichkeiten das umzusetzen. Z.B. in ComputeMarketArea könnte auch der CancellationToken übergeben werden um dort kooperativ abzubrechen.

Aber letztlich reicht ein await Task.Run aus um die CPU-Aufgabe zum ThreadPool zu verlagern, ohne dabei irgendwelche Fehler nicht behandeln zu können. Probiers einmal aus.

Bzgl. des httpClient meinst du das hier, zwar nicht HttpClientFactory aber inhaltlich sehr ähnlich?

Genau 😃

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!"

T
tobi45f Themenstarter:in
59 Beiträge seit 2017
vor 4 Jahren

Danke für die ausführliche Antwort! Echt klasse 😃

Der lange andauernde Prozess ist ein Berechnungsalgorithmus bestehende aus einer Schleife mit: Datenbanken lesen, Werte Interpretieren, eine Schnittstelle zu einem Programm ansteuern (Exe, macht den größten Teil aus, je Iteration ne knappe Sekunde), Datenbank wieder lesen, interpretieren und schreiben. Es ist also quasi ein Gemisch aus CPU und IO.

Ich hatte tatsächlich mein Workaround auch schon so umgeschrieben, dass die Berechnung als Task gestartet wird und nachher das Ergebnis async ohne eigenen Task als json gepostet wird 😃 Das Fenster bleibt nicht hängen und mein Code läuft wie erwünscht. Das stimmt mich grade positiv, dass ich das sogar schon richtig interpretiert hatte 😉 Danke für den Code zum Verständnis und zur Kontrolle!

Jetzt ist der Code zwar nicht mehr so Chronologisch übersichtlich im Ablauf, da er mit den Aufrufen etwas verschachtelt ist, aber er läuft, ist korrekt und das zählt 😄

Vielen lieben Danke für die Hilfe!
Gruß Tobias

6.911 Beiträge seit 2009
vor 4 Jahren

Hallo tobi45f,

ein Berechnungsalgorithmus bestehende aus einer Schleife mit: Datenbanken lesen, Werte Interpretieren, ...

Somit kannst du beim DB-Lesen asynchron vorgehen.

Auch den externen Prozess kannst du asynchron "erwarten". Dazu brauchst du nur eine Erweiterungsmethode, siehe Bsp.:


using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static async Task Main(string[] args)
        {
            int exitCode = await Process.Start("cmd.exe");
            Console.WriteLine($"exit code: {exitCode}");
        }
    }

    internal static class ProcessExtensions
    {
        public static TaskAwaiter<int> GetAwaiter(this Process process)
        {
            var tcs = new TaskCompletionSource<int>();
            process.EnableRaisingEvents = true;

            process.Exited += (s, e) => tcs.TrySetResult(process.ExitCode);
            if (process.HasExited) tcs.TrySetResult(process.ExitCode);

            tcs.Task.ContinueWith(
                (_, p) => (p as Process).Dispose(),
                process,
                TaskContinuationOptions.ExecuteSynchronously);

            return tcs.Task.GetAwaiter();
        }
    }
}

etzt ist der Code zwar nicht mehr so Chronologisch übersichtlich im Ablauf,

Das lässt sich aber wieder übersichtlicher gestalten, ev. durch gezieltes Refactoring.
Leserlicher Code ist sehr wichtig und auch dass sinnvolle Bezeichner verwendet werden.

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!"

W
955 Beiträge seit 2010
vor 4 Jahren

etzt ist der Code zwar nicht mehr so Chronologisch übersichtlich im Ablauf,
Das lässt sich aber wieder übersichtlicher gestalten, ev. durch gezieltes Refactoring. Wenns noch komplexer wird kann man weitere Abstraktionsmechanismen einsetzen wie z.B. RX oder Dataflow.

T
tobi45f Themenstarter:in
59 Beiträge seit 2017
vor 4 Jahren

Bisher ist meine Methode mit dem Prozess nicht asynchron gelöst. Wenn das sinnvoll ist, dann würde ich mich dem nicht in den Weg stellen 😉 Allerdings weiß ich nicht so ganz, wie ich das ändern muss.

Den GetAwaiter-Code verstehe ich leider gar nicht 😄 Aber ich glaube, dass ich das nicht so umsetzen kann, da ich den Konsoleninhalt auslese, um das Simulationsergebnis zu interpretieren. oder kann ich ohne neue Instanz von Process die Konsole lesen?

Und: wenn ich doch jetzt meinen Aufruf für die Exe Async mache, dann habe ich doch wieder das Problem, dass ich in meinen langen "berechnungs-Task" einen async Task<bool> Aufruf habe, der dann wiederum meinen "Aufrufer-Task" beeinflusst, so wie es vorher mit dem Posten der Json File war?

Aufruf aus MainWindow:

private async void Btn_Matching(object sender, RoutedEventArgs e)
        {            
            tokenSource = new CancellationTokenSource();
            var token = tokenSource.Token;



            try
            {
                TradeBids tradeBids;
                tradeBids = await berechnung.GetTAsync<TradeBids>(pathTradeBids);

                TradeSolutions tradeSolutions = await Task<TradeSolutions>.Factory.StartNew(() => berechnung.Matching(token, tradeBids, Convert.ToInt32(tb_Laufzeit.Text)), 
                    token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

                if (tradeSolutions == null)
                {
                    tb_ausgabe.Text += "Fehler beim Matching.\n";
                    return;
                }

                bool res = await berechnung.PostTAsync(pathDemand, tradeSolutions);
                if (res) tb_ausgabe.Text += $"Die Ergebnisse der Optimierung wurden an den Server übermittelt\nFür Marktgebiet " +
                        $"{GetMarktbereichFromID(Convert.ToInt32(tradeSolutions.Marktbereich))} wurden bei insgesamt {tradeSolutions.SolutionList.Count()}" +
                        $" technische Lösungen übermittelt. Die Monetäre Auswertung erfolgt über die Plattform.";
            }
            catch (TaskCanceledException etask)
            {
                tb_ausgabe.Text += (etask.Message) + "\n";
            }
            catch (OperationCanceledException)
            {
                tb_ausgabe.Text += "Das Matching wurde vom Nutzer abgebrochen\n";
            }
            catch (HttpRequestException eHTTP)
            {
                tb_ausgabe.Text += (eHTTP.Message) + "\n";
            }
            catch (Exception ex)
            {
                tb_ausgabe.Text += (ex.Message) + "\n";
            }
            finally
            {
                Reset();
            }
        }

Hier haben wir ja die Tasks von GetTAsync und PostTAsync aus dem await Task<TradeSolutions>.Factory.StartNew(() rausgezogen, damit nicht der API-Kram nicht den Task beendet.

In der Matching Methode passiert nicht viel von Bedeutung, aber es wird die Methode Simulate aufgerufen (bisher normal ohne Task)


private bool Simulate(string command)
        {
            ProcessStartInfo processStartInfo = new ProcessStartInfo(command);
            processStartInfo.RedirectStandardOutput = true;
            processStartInfo.UseShellExecute = false;
            processStartInfo.CreateNoWindow = true;
            Process process = new Process();
            process.StartInfo = processStartInfo;
            process.Start();
            string result = process.StandardOutput.ReadToEnd();
//Verarbeitung des results und dementsprechende Rückgabe
}

Wenn ich doch jetzt bool Simulate in async Task<bool> Simulate umwandele, dann habe ich doch dasselbe Problem, dass ich vorher mit dem Posten/Abrufen der JSon files hatte, da es ja ein async Task in einem Task war?

16.806 Beiträge seit 2008
vor 4 Jahren

da es ja ein async Task in einem Task war?

Was Du oben gemacht hast ist, dass Du eine asynchrone Operation erneut in einen Task gepackt hast - also verschachtelt.
Ich sehe jetzt nirgends die Stelle, bei der Du Simulate verwendest.

Aber hier als Vergleich:

Entweder:

await Task.Run(() => berechnung.Simulate(...)); // Simulate ist nicht async, muss daher gewrapped werden.

oder (besser)

await berechnung.SimulateAsync(...); // Simulate ist  async, alles sauber

Hinweise:
Weil Du MainWindow schreibst: in WPF sollte man unbedingt mit dem MVVM Pattern arbeiten. Ansonsten wirst Du von Workaround zu Workaround stolpern.
[Artikel] MVVM und DataBinding
WPF ist einfach darauf ausgelegt.

Sowas wie

if (res) tb_ausgabe.Text

ist extrem schlecht zu lesen. Verwende Scopes!

Wenn Du Long Running Tasks hast, dann kannst einfach Task.Run() verwenden statt über die Factory zu gehen; außer Du brauchst die Create Options von StartNew.

   if (tradeSolutions == null)
                {
                    tb_ausgabe.Text += "Fehler beim Matching.\n";

Übliche Signaturverhalten wäre hier so, dass tradeSolutions == null suggeriert, dass es keine TradeSolution gibt.
Ein Fehler würde man eigentlich - wie in Deinen anderen Fällen - mit Exceptions quittieren (oder dem Try-Pattern).

6.911 Beiträge seit 2009
vor 4 Jahren

Hallo tobi45f,

Den GetAwaiter-Code verstehe ich leider gar nicht

Ganz grob: wenn der C#-Compiler ein await sieht, so verlangt er nach einem Typen der eine GetAwaiter-Methode (auch als Erweiterungsmethode) hat und diese Methode muss ein "awaitable" zurückgeben. Das ist z.B. bei Task der Fall.
Hier haben wir einen Process und wollen diesen asynchron erwarten, da der C#-Compiler jedoch nicht weiß was und wie er dies tun soll, helfen wir ihm mit dieser Erweiterungsmethode.

dass ich das nicht so umsetzen kann, da ich den Konsoleninhalt auslese, um das Simulationsergebnis zu interpretieren.

Ja so, kann die Erweiterungsmethode nicht 1:1 verwendet werden, da nur der ExitCode zurückgegeben wird und nicht die Ausgabe aus stdout. Dazu muss diese aus meinem vorigen Post angepasst werden:


using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static async Task Main(string[] args)
        {
            string stdout = await "cmd.exe".ReadStdOut();
            Console.WriteLine($"stdout: {stdout}");
        }
    }

    internal static class ProcessExtensions
    {
        public static Process ReadStdOut(this string command)
        {
            var startInfo = new ProcessStartInfo(command);
            startInfo.RedirectStandardOutput = true;
            startInfo.UseShellExecute = false;
            startInfo.CreateNoWindow = true;

            var process = new Process();
            process.StartInfo = startInfo;
            process.Start();

            return process;
        }

        public static TaskAwaiter<string> GetAwaiter(this Process process)
        {
            var tcs = new TaskCompletionSource<string>();
            process.EnableRaisingEvents = true;

            process.Exited += (s, e) => tcs.TrySetResult(process.StandardOutput.ReadToEnd());
            if (process.HasExited) tcs.TrySetResult(process.StandardOutput.ReadToEnd());

            tcs.Task.ContinueWith(
                (_, p) => (p as Process).Dispose(),
                process,
                TaskContinuationOptions.ExecuteSynchronously);

            return tcs.Task.GetAwaiter();
        }
    }
}

(als Beispiel, kannst und sollst du natürlich anpassen wie es dir passt).

aus dem await Task<TradeSolutions>.Factory.StartNew(() rausgezogen, damit nicht der API-Kram nicht den Task beendet.

Das hatten wir deshalb gemacht, damit ein Fehler im "inneren Task" auch beobachtet werden kann. Durch Task.Factory.StartNew war das nicht der Fall, daher ist die App gecrasht.
Wenn hier nur der Process per await erwartet wird, so gibt es nur einen Task und hat dieser einen Fehler, so wird dieser auch gefangen. Kein Problem.
Allerdings ist in obiger (meiner) Umsetzung vom GetAwaiter für den Process nichts eingebaut um den Task in eine Fehler-Zustand zu setzen (ginge via tcs.TrySetException, das kannst du ja selbst machen -- siehe angehängtes Beispiel)

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!"

T
tobi45f Themenstarter:in
59 Beiträge seit 2017
vor 4 Jahren

Vielen Dank, Gü, für den Code! Mit einigen kleinen Anpasungen läuft es jetzt!

Was Du oben gemacht hast ist, dass Du eine asynchrone Operation erneut in einen Task gepackt hast - also verschachtelt.
Ich sehe jetzt nirgends die Stelle, bei der Du Simulate verwendest.

Ich habe die Matching-Mathode jetzt nur async aufgerufen und die Simulate Methode um den GetAwaiter erweitert. So scheint es zu laufen!

Den MVVM und DataBinding Artikel muss ich mir mal in einer ruhigen Minute zu Gemüte führen. TryGet kannte ich auch noch nicht, ist aber sicherlich sinnvoller als das, was ich mache 😉 danke für die Hinweise! Und Danke generell für die Hilfe!

Gruß Tobias

edit:
das einzige was mir jetzt auffällt, was nicht optimal ist ist, dass das Fenster zwar nicht einfriert und sich verschieben etc. lässt. Jedoch stockt es so alle 500ms für einen kurzen Moment und geht dann weiter. Als das mit dem neuen Task lieft, da war das nicht. Dh. dass der sonstige Code der zwischen dem async Prozess-Aufruf läuft, das Fenster kurz blockiert, richtig? Vermutlich muss ich das Datenbank lesen+schreiben noch asyncen?!

6.911 Beiträge seit 2009
vor 4 Jahren

Hallo tobi45f,

richtig? Vermutlich muss ich das Datenbank lesen+schreiben noch asyncen?!

Ja, alles async -- async ist viral 😃

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!"

T
tobi45f Themenstarter:in
59 Beiträge seit 2017
vor 4 Jahren

Hm so richtig flüssig ist es dennoch nicht. Es hakt immernoch beim bewegen. Eher eine kosmetische Sache als eine notwendige.
Ich habe einfach die Methode asyc gemacht und die DB mit await geöffnet (await conn.OpenAsync();) und auch den reader von oledbreader auf dbdatareader geändert um async zu lesen (var rdr = await cmd.ExecuteReaderAsync();) bzw. zum schreiben (await cmd.ExecuteNonQueryAsync();)

Fehlt noch was? Ich habe auf jeden fall keine synchronen Aufrufe für den DB-Kram mehr im Code.

6.911 Beiträge seit 2009
vor 4 Jahren

Hallo tobi45f,

Fehlt noch was?

Ich kenn deinen Code nicht, daher schwer genau zu sagen was es ist.
Bedenke jedoch dass der Code bis zum ersten await synchron ausgeführt wird und dann hängt es davon abder erwartete Task schon fertig ist, d.h. es wird synchron weitergearbeitet der erwartete Task nicht fertig ist und im Hintergrund (asynchron) ausgeführt wird

Du kannst versuchsweise ziemlich am Beginn der Methode ein await Task.Yield() einbauen um async-Ausführung zu "erzwingen" und schauen ob es besser wird.

* der Einfachheithalber schreib ich nur Task, korrekter wäre "Awaitable" zu dem neben Task, ValueTask auch jedes selbst implementierte Awaitable gehört.

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!"