Laden...

CancellationTokenSource CancelAfter hat keinen Effekt, Task wird trotzdem abgebrochen

Letzter Beitrag vor 3 Jahren 18 Posts 707 Views
CancellationTokenSource CancelAfter hat keinen Effekt, Task wird trotzdem abgebrochen

Hallo,

ich habe folgendes Problem: Ich habe eine Klasse zur seriellen Komunikation (SerialPort) mit einem Gerät. Diese schickt eine Anfrage und wartet auf einen kompletten Frame. Der Frame muss aber nicht auf einmal als Paket kommen, sondern kann auch mal länger sein. Nach jedem DataReceivedEvent packe ich die empfangenen Daten in eine globale Liste, schaue ob Anfangs- und Endekennung da sind und gebe den Rahmen zurück, wenn nicht resete ich den Timeout (CancelAfter) und warte weiter auf das Ende des Frames.

Hinter dem Requestable_Object im EventHandler verbirgt sich ein SerialPort. Mein Problem ist nun, dass die CancellationTokenSource auslöst, obwohl ich immer im EventHandler
CancelAfter aufrufe. Das scheint keinen Effekt zu haben. Ich habe mir mal ein Minimalbeispiel mit Task.Delay und Buttons zum Rücksetzten und Viewmodel geschrieben, da ging es.

Woran liegt das?

Danke und Grüße, Alex


private readonly TaskCompletionSource<byte[]> _TaskCompletionSource;
private CancellationTokenSource _CancellationTokenSource;

private void SetTimeout()
{
      _CancellationTokenSource = new CancellationTokenSource(_Timeout);
      _CancellationTokenSource.Token.Register(() => CancelRequest());
}

private bool CancelRequest()
{
      return _TaskCompletionSource.TrySetResult(Array.Empty<byte>());
}

private void ResetTimeout()
{
      try
      {
          _CancellationTokenSource.CancelAfter(_Timeout);
      }
      catch (ObjectDisposedException) { }
}

public async Task ProcessRequest()
{
      SendRequest();
      SetTimeout();
      Data = await GetResponseAsync();
}

private void RequestableObject_DataAvailable(object sender, RequestDataEventArgs e)
{
     ResetTimeout();
     AddData(e.Data);
    
     byte[] data = _ReceivedData.ToArray();

     if (_ProtocolDecoder.IsFullFrame(data) && _ProtocolDecoder.TryDecode(data, out byte[] outData) == FrameCheckResult.Frame)
     {
           _ = _TaskCompletionSource.TrySetResult(outData);
     }
}

Final no hay nada más

Hallo ill_son ,

resete ich den Timeout (CancelAfter)

Das geht bei einer CancellationTokenSource nicht* immer. Sie ist sozusagen "one way" falls einmal gesetzt bzw. gecanceled und kann dann nicht mehr zurückgesetzt werden. Trifft das zu? Daher hat es womöglich auch keinen Effekt.

Erstell besser eine neue CTS für jeden Request-Vorgang mit entsprechendem Timeout.

* CancellationTokenSource.TryReset wurde für diese Zwecke mit .NET 6 eingeführt.

Nach jedem DataReceivedEvent packe ich die empfangenen Daten in eine globale Liste, schaue ob Anfangs- und Endekennung da sind und gebe den Rahmen zurück

Das wäre ein Anwendungsfalls für System.IO.Pipelines in .NET.

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

Hallo gfoidl,

Das wäre ein Anwendungsfalls für System.IO.Pipelines in .NET.

Danke, das schaue ich mir auf jeden Fall an. Löst das mein Resetproblem? Ich hätte vielleicht noch erwähnen sollen, dass ich mit Binärdaten arbeite.

Die oben beschriebene Klasse ist pro Request. Nur kann eben die Antwort auf mehrere Events verteilt sein und ich setzte dann die CancellationTokenSource zurück, weil ich ja weiter warten muss. Wenn sie ausgelöst hat, dann ist der Request abgebrochen und beendet.

folgendes Minimalbeispiel hat bei mir funktioniert, weshalb ich denke, dass es was mit dem Event zutun hat (Context oder so?):

Edit: CancellationTokenSource.TryReset kann ich hier gar nicht finden (.NET Framework 4.8)


private CancellationTokenSource cts;

public IAsyncCommand StartCommand { get; } = new AsyncCommand(Start);
public ICommand ResetCommand { get; } = new RelayCommand(Reset);

private async Task Start()
{
    cts?.Dispose();
    cts = new CancellationTokenSource(5000);
    Task.Delay(1000000000, cts); //warte echt lange
}

private void Reset(object dummy)
{
    try
    {        
        cts?.CancelAfter(5000);
    }
    catch { }
}

Wenn ich einmal den Start- und anschließend regelmäßig den Reset-Button gedrückt habe, hat die cts nicht ausgelöst. Und nichts anderes passiert in meiner Request-Klasse, nur dass da der Reset aus dem SerialPort-Thread kommt.

Final no hay nada más

Erneutes TryCancel scheint zu funktionieren (hab's gerade nochmal getestet), aber auch nur, wenn die Zeit noch nicht abgelaufen ist.

Aber Du darfst auch nicht vergessen, dass das Event ständig feuert, je nach Einstellung also sehr oft in sehr kurzer Zeit - das Handling verschiedener Threads wird also nicht einfacher 😉

Aber warum liest Du nicht einfach synchron?
Ich habe etwas ähnliches auch erst mit dem Event versucht, habe es dann aber verworfen, weil das Handling unnötig kompliziert wurde.
Jetzt ist es eine Schleife, die immer liest, solange bis die Nachricht vollständig ist und dann eine TaskCompletionSource setzt.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Aber warum liest Du nicht einfach synchron?

Meint synchron, im UI-Thread? Weil die Übertragung vom dem Gerät auch mal etwas länger dauern kann, wenn große Datenmengen anfallen.

Final no hay nada más

"Synchron" meint "nicht asynchron" und ohne Event.
Eine Schleife, die solange synchron liest, bis alles zusammen ist oder abgebrochen wurde.
In welchem Thread das effektiv läuft, ist ein anderes Thema, aber es muss natürlich nicht der UI-Thread sein.

Ich hab bei einem aktuellen Projekt auch den Fehler gemacht, es asynchron umsetzen zu wollen.
Erster Versuch war auch mit dem Event und einer TaskCompletionSource. Ich weiß nicht mehr genau, was das Fass zum Überlaufen gebracht hat, aber am Ende wurde es mir einfach zu kompliziert und ich habe es verworfen.
Danach habe ich den SerialStream (BaseStream-Property vom SerialPort) verwendet, da der asynchron lesen und schreiben kann, allerdings scheint der einen Bug in Verbindung mit dem CancellationToken zu haben.
Jetzt ist es eine Endlos-Schleife und eine Queue mit abzuarbeitenden Operationen (Read oder Write). Die Schleife liest solange, bis die eine Operation fertig ist und ignoriert dabei TimeoutExceptions, die der SerialPort ggf. wirft. Ich habe mich dabei ein bisschen an der SerialStream-Implementierung und die IORequests von Microsoft orientiert.

Und jetzt bin ich so weit zu sagen, dass das ganze Thema "Asynchron" beim SerialPort ein Fehler war 😁
Bei mir kommt nach dem Read noch einiges an Komplexität oben drauf, die bei dem aktuellen Stand auch asynchron arbeiten muss.
Einfacher wäre eine vollständig synchroner Ablauf, der in einem BackgroundService (oder schlicht: while-Schleife im eigenen Thread) läuft und normal synchron alles abarbeitet.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Das kann ich mal probieren. Mein eigentlichen Problem war ja das Timeout. Wenn was unvorhergesehenes passiert (Gerät hört auf zu senden), dann soll der Vorgang nach einer Weile abgebrochen werden. Ich weiß auch vorher nicht, wie lange das insgesamt dauert, weil ich die Datenmenge nicht kenne. Deshalb brauche ich ein Timeout, dass ich immer, wenn noch Daten kommen, wieder zurücksetzten kann.

Final no hay nada más

Ich hatte ein ähnliches Problem:
Wenn dem Gerät irgendetwas nicht passt, bekomme ich keinen Fehler, sondern einfach gar keine Antwort ^^
Damit muss der Code natürlich umgehen können, wofür ich dann - wie Du - die CancelAfter-Methode nutze.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Und da sind wir wieder beim meinem Problem. Trotz aufrufen dieser Methode im DataReceived event wird mein Task abgebrochen.

Aus der Doku

Subsequent calls to CancelAfter will reset the delay for this CancellationTokenSource, if it has not been canceled already.

Und genau das klappt bei mir nicht.

Final no hay nada más

Folgender Test-Code:


var cts = new CancellationTokenSource();

cts.Token.Register(() => Console.WriteLine("Canceled"));

Task.Run(() =>
{
    var i = 40;

    while (!cts.Token.IsCancellationRequested)
    {
        Thread.Sleep(i * 10);
        Console.WriteLine("Tik: " + ++i);

        try
        {
            cts.CancelAfter(500);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
            throw;
        }
    }

    Console.WriteLine("Finished");
});

Console.WriteLine("Started...");
Console.ReadKey();

Bricht - wie erwartet - bei ca. 50 ab.
Das Problem liegt also nicht am CancelAfter, sondern an irgendwas Anderem.

Wird das Event denn wirklich mehrfach aufgerufen oder "hängt" der erste Durchlauf?
Läuft er vielleicht in einen eigenen Timeout? Der SerialPort behandelt einen eigene Timeout, den man einstellen kann.
Oder es passiert irgendwas anderes, was nicht im gezeigten Code steht.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Mein obiges Minimalbeispiel funktioniert ja auch, weshalb auch ich die Vermutung habe, dass es damit zu tun hat, dass das CancelAfter im SerialPort Event aufgerufen wird. Aber da steck ich nicht tief genug in der Materie. Ich mach nichts weiter also oben beschrieben: Daten im EventHandler lesen, prüfen und CancelAfter aufrufen. Wenn der Frame fertig ist, TaskCompleationSource setzen. Wenn die Datenmenge groß ist, kommt das Event mehrfach. Wenn ich das Timeout nicht verwende (CancellationTokenSource - Konstruktor ohne Argument), dann läuft die Übertragung durch.

Final no hay nada más

Der SerialPort tut aber nichts mit dem CTS - wie soll es das denn auch tun, ohne eine Referenz zu haben?

Aber es wird - je nach Art zu lesen - eine eigene TimeoutException, wenn der eigene Timeout abgelaufen ist - oder nicht, wenn es Infinite ist.
Wenn Du über den Buffer hinaus liest, läufst Du also entweder in eine andere TimeoutException oder in ein ewiges Warten auf Daten.
Aber das hast Du nicht gezeigt.

Oder anders:
Wird das ResetTimeout wirklich mehrfach aufgerufen?
Ist die Zeit zwischen den ResetTimeout-Aufrufen vielleicht zu zu lang - vielleicht passend zum Timeout des SerialPorts?

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Schau mal in meinen Anfangspost. Da wird im EventHandler eine Funktion ResetTimeout aufgerufen. Die greift auf die CancellationTokenSource zu.

Final no hay nada más

Und wer ruft den EventHandler auf?
Und wie oft wird der aufgerufen?
Und wie viel Zeit liegt zwischen den Aufrufen?

Der Code, der darüber entscheidet, steht nicht im Anfangspost.
Das musst Du testen, aber ohne Debuggen, denn das würde den Test verfälschen.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Der EventHandler wird vom SerialPort im DataReceived-Event aufgerufen. Häufigkeit habe ich mir mal mit einer StopWatch mal ausgemessen. Aller paar 100 ms. Das schwankt natürlich etwas.

Final no hay nada más

Ich habe noch etwas festgestellt. Wenn ich die CancellationTokenSource ohne Timeout erstelle und CancelAfter nur im EventHandler aufrufe funtioniert das zurücksetzten. Wenn ich den parameterbehafteten Konstruktor verwende oder CancelAfter direkt nach dem Erstellen der cts aufrufe, funktioniert CancelAfter im EventHandlers nicht.

Final no hay nada más

Ich habe noch etwas festgestellt. Wenn ich die CancellationTokenSource ohne Timeout erstelle und CancelAfter nur im EventHandler aufrufe funtioniert das zurücksetzten. Wenn ich den parameterbehafteten Konstruktor verwende oder CancelAfter direkt nach dem Erstellen der cts aufrufe, funktioniert CancelAfter im EventHandlers nicht.

Das Verhalten tritt im Test-Projekt aber auch nicht auf.

Was mir dabei aber auch auffällt:
Deine SetTimeout-Methode disposed die CancellationTokenSource nicht.
Also nur, weil Du die alte Referenz durch eine Neue ersetzt, heißt das nicht, dass der interne Timeout auch aufhört.
Eventuell liegt hier also noch eine CancellationTokenSource herum, die nicht verworfen wurde und dann "unbeobachtet" deinen Task abbricht?

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Die Klasse, die den Request kapselt und in der sich die CancellationTokenSource befindet, implementiert IDisposable und disposed dann auch die cts. Die wird auch nur einmal an einer Stelle erstellt und nicht mehrmals.

Final no hay nada más