548 Stimmen

Asynchrones Warten auf die Fertigstellung von Task<T> mit Timeout

Ich möchte auf eine Aufgabe<T> mit einigen Sonderregeln zu ergänzen: Wenn der Vorgang nach X Millisekunden noch nicht abgeschlossen ist, möchte ich dem Benutzer eine Meldung anzeigen. Und wenn er nach Y Millisekunden noch nicht abgeschlossen ist, möchte ich automatisch Stornierung beantragen .

Ich kann Task.ContinueWith um asynchron auf den Abschluss der Aufgabe zu warten (d.h. eine Aktion zu planen, die ausgeführt wird, wenn die Aufgabe abgeschlossen ist), aber das erlaubt es nicht, einen Timeout anzugeben. Ich kann verwenden Aufgabe.Warten synchron zu warten, bis die Aufgabe mit einem Timeout abgeschlossen ist, aber das blockiert meinen Thread. Wie kann ich asynchron warten, bis die Aufgabe mit einer Zeitüberschreitung abgeschlossen ist?

1voto

sjb-sjb Punkte 1052

Ein paar Varianten der Antwort von Andrew Arnott:

  1. Wenn Sie auf eine bestehende Aufgabe warten und herausfinden möchten, ob sie abgeschlossen wurde oder eine Zeitüberschreitung eingetreten ist, sie aber nicht abbrechen möchten, wenn die Zeitüberschreitung eintritt:

    public static async Task<bool> TimedOutAsync(this Task task, int timeoutMilliseconds)
    {
        if (timeoutMilliseconds < 0 || (timeoutMilliseconds > 0 && timeoutMilliseconds < 100)) { throw new ArgumentOutOfRangeException(); }
    
        if (timeoutMilliseconds == 0) {
            return !task.IsCompleted; // timed out if not completed
        }
        var cts = new CancellationTokenSource();
        if (await Task.WhenAny( task, Task.Delay(timeoutMilliseconds, cts.Token)) == task) {
            cts.Cancel(); // task completed, get rid of timer
            await task; // test for exceptions or task cancellation
            return false; // did not timeout
        } else {
            return true; // did timeout
        }
    }
  2. Wenn Sie eine Arbeitsaufgabe starten und die Arbeit abbrechen möchten, wenn die Zeitüberschreitung auftritt:

    public static async Task<T> CancelAfterAsync<T>( this Func<CancellationToken,Task<T>> actionAsync, int timeoutMilliseconds)
    {
        if (timeoutMilliseconds < 0 || (timeoutMilliseconds > 0 && timeoutMilliseconds < 100)) { throw new ArgumentOutOfRangeException(); }
    
        var taskCts = new CancellationTokenSource();
        var timerCts = new CancellationTokenSource();
        Task<T> task = actionAsync(taskCts.Token);
        if (await Task.WhenAny(task, Task.Delay(timeoutMilliseconds, timerCts.Token)) == task) {
            timerCts.Cancel(); // task completed, get rid of timer
        } else {
            taskCts.Cancel(); // timer completed, get rid of task
        }
        return await task; // test for exceptions or task cancellation
    }
  3. Wenn Sie bereits eine Aufgabe erstellt haben, die Sie im Falle einer Zeitüberschreitung abbrechen möchten:

    public static async Task<T> CancelAfterAsync<T>(this Task<T> task, int timeoutMilliseconds, CancellationTokenSource taskCts)
    {
        if (timeoutMilliseconds < 0 || (timeoutMilliseconds > 0 && timeoutMilliseconds < 100)) { throw new ArgumentOutOfRangeException(); }
    
        var timerCts = new CancellationTokenSource();
        if (await Task.WhenAny(task, Task.Delay(timeoutMilliseconds, timerCts.Token)) == task) {
            timerCts.Cancel(); // task completed, get rid of timer
        } else {
            taskCts.Cancel(); // timer completed, get rid of task
        }
        return await task; // test for exceptions or task cancellation
    }

Noch eine Anmerkung: Diese Versionen brechen den Timer ab, wenn die Zeitüberschreitung nicht eintritt, so dass mehrere Aufrufe nicht zu einer Anhäufung von Timern führen.

sjb

1voto

antak Punkte 17136

Ich spürte die Task.Delay() Aufgabe und CancellationTokenSource in den anderen Antworten ein bisschen viel für meinen Anwendungsfall in einer engen Netzwerkschleife.

Und obwohl Joe Hoags Crafting a Task.TimeoutAfter-Methode auf MSDN-Blogs inspirierend war, war ich ein wenig müde, die TimeoutException für die Flusskontrolle aus demselben Grund wie oben, weil Zeitüberschreitungen häufiger erwartet werden als nicht.

Deshalb habe ich mich für diese Lösung entschieden, die auch die im Blog erwähnten Optimierungen vornimmt:

public static async Task<bool> BeforeTimeout(this Task task, int millisecondsTimeout)
{
    if (task.IsCompleted) return true;
    if (millisecondsTimeout == 0) return false;

    if (millisecondsTimeout == Timeout.Infinite)
    {
        await Task.WhenAll(task);
        return true;
    }

    var tcs = new TaskCompletionSource<object>();

    using (var timer = new Timer(state => ((TaskCompletionSource<object>)state).TrySetCanceled(), tcs,
        millisecondsTimeout, Timeout.Infinite))
    {
        return await Task.WhenAny(task, tcs.Task) == task;
    }
}

Ein Beispiel für einen Anwendungsfall ist folgendermaßen:

var receivingTask = conn.ReceiveAsync(ct);

while (!await receivingTask.BeforeTimeout(keepAliveMilliseconds))
{
    // Send keep-alive
}

// Read and do something with data
var data = await receivingTask;

0voto

tm1 Punkte 1085

Ich fasse die Ideen einiger anderer Antworten hier zusammen und diese Antwort in einem anderen Thread in eine Try-ähnliche Erweiterungsmethode. Dies ist von Vorteil, wenn Sie eine Erweiterungsmethode wünschen, aber eine Ausnahme bei Zeitüberschreitung vermeiden wollen.

public static async Task<bool> TryWithTimeoutAfter<TResult>(this Task<TResult> task,
    TimeSpan timeout, Action<TResult> successor)
{

    using var timeoutCancellationTokenSource = new CancellationTokenSource();
    var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token))
                                  .ConfigureAwait(continueOnCapturedContext: false);

    if (completedTask == task)
    {
        timeoutCancellationTokenSource.Cancel();

        // propagate exception rather than AggregateException, if calling task.Result.
        var result = await task.ConfigureAwait(continueOnCapturedContext: false);
        successor(result);
        return true;
    }
    else return false;        
}     

async Task Example(Task<string> task)
{
    string result = null;
    if (await task.TryWithTimeoutAfter(TimeSpan.FromSeconds(1), r => result = r))
    {
        Console.WriteLine(result);
    }
}

0voto

Theodor Zoulias Punkte 22972

Hier ist eine Low-Level-Implementierung eines WaitAsync Methode, die sowohl eine Zeitüberschreitung als auch eine CancellationToken und überträgt im Falle einer Ausnahme alle Fehler des Task<T> und nicht nur den ersten Fehler:

public static Task<TResult> WaitAsync<TResult>(this Task<TResult> task,
    TimeSpan timeout, CancellationToken cancellationToken = default)
{
    if (task == null) throw new ArgumentNullException(nameof(task));
    if (timeout < TimeSpan.Zero && timeout != Timeout.InfiniteTimeSpan)
        throw new ArgumentOutOfRangeException(nameof(timeout));

    var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
    cts.CancelAfter(timeout);

    return task
        .ContinueWith(_ => { }, cts.Token,
            TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default)
        .ContinueWith(continuation =>
        {
            cts.Dispose();
            if (task.IsCompleted) return task;
            cancellationToken.ThrowIfCancellationRequested();
            if (continuation.IsCanceled) throw new TimeoutException();
            return task;
        }, TaskScheduler.Default).Unwrap();
}

A TimeoutException wird ausgelöst, wenn die Zeitüberschreitung vor Beendigung der task .

Ehrlich gesagt ist die Weitergabe aller Fehler in diesem Fall nicht wirklich eine wertsteigernde Funktion. Der Grund dafür ist, dass wenn Sie die WaitAsync wie diese: await someTask.WaitAsync(timeout) werden alle zusätzlichen Fehler von der Software geschluckt. await Operator, der absichtlich nur die erste Ausnahme der erwarteten Aufgabe weitergibt. Und es macht nicht viel Sinn, die WaitAsync Aufgabe in einer Variablen zu speichern und sie innerhalb der catch Block, denn Sie haben bereits den someTask zur Verfügung, und Sie könnten dies stattdessen untersuchen.

0voto

kns98 Punkte 330

Wenn Sie eine BlockingCollection verwenden, um die Aufgabe zu planen, kann der Producer die potenziell lang laufende Aufgabe ausführen und der Consumer kann die TryTake-Methode verwenden, die eine Zeitüberschreitung und ein Abbruch-Token enthält.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X