Warten Sie asynchron, bis Task <T> mit Timeout abgeschlossen ist


387

Ich möchte auf eine warten Aufgabe <T> mit einigen speziellen Regeln abgeschlossen ist: Wenn sie nach X Millisekunden nicht abgeschlossen wurde, möchte ich dem Benutzer eine Nachricht anzeigen. Und wenn es nach Y Millisekunden nicht abgeschlossen ist, möchte ich automatisch eine Stornierung beantragen .

Ich kann Task.ContinueWith verwenden, um asynchron auf den Abschluss der Aufgabe zu warten (dh eine Aktion zu planen, die ausgeführt werden soll, wenn die Aufgabe abgeschlossen ist), aber dies erlaubt keine Angabe eines Zeitlimits. Ich kann Task.Wait verwenden, um 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?


3
Du hast recht. Ich bin überrascht, dass es keine Zeitüberschreitung gibt. Vielleicht in .NET 5.0 ... Natürlich können wir das Timeout in die Aufgabe selbst einbauen, aber das ist nicht gut, solche Dinge müssen frei kommen.
Aliostad

4
Während für das von Ihnen beschriebene zweistufige Zeitlimit weiterhin Logik erforderlich wäre, bietet .NET 4.5 in der Tat eine einfache Methode zum Erstellen eines zeitlimitbasierten Zeitlimits CancellationTokenSource. Dem Konstruktor stehen zwei Überladungen zur Verfügung, eine mit einer Verzögerung von ganzzahligen Millisekunden und eine mit einer TimeSpan-Verzögerung.
Patridge

Die vollständige einfache lib-Quelle finden Sie hier: stackoverflow.com/questions/11831844/…

Gibt es eine endgültige Lösung, bei der der vollständige Quellcode funktioniert? Vielleicht komplexeres Beispiel für Benachrichtigungsfehler in jedem Thread und nachdem WaitAll eine Zusammenfassung angezeigt hat?
Kiquenet

Antworten:


565

Wie wäre es damit:

int timeout = 1000;
var task = SomeOperationAsync();
if (await Task.WhenAny(task, Task.Delay(timeout)) == task) {
    // task completed within timeout
} else { 
    // timeout logic
}

Und hier ist ein großartiger Blog-Beitrag "Crafting a Task.TimeoutAfter Method" (vom MS Parallel Library-Team) mit weiteren Informationen zu solchen Dingen .

Ergänzung : Auf Anfrage eines Kommentars zu meiner Antwort finden Sie hier eine erweiterte Lösung, die die Bearbeitung von Stornierungen umfasst. Beachten Sie, dass das Übergeben der Stornierung an die Aufgabe und den Timer bedeutet, dass es mehrere Möglichkeiten gibt, die Stornierung in Ihrem Code zu erfahren. Sie sollten sicher sein, dass Sie alle Stornierungen testen und sicher sein, dass Sie alle ordnungsgemäß verarbeiten. Überlassen Sie nicht dem Zufall verschiedene Kombinationen und hoffen Sie, dass Ihr Computer zur Laufzeit das Richtige tut.

int timeout = 1000;
var task = SomeOperationAsync(cancellationToken);
if (await Task.WhenAny(task, Task.Delay(timeout, cancellationToken)) == task)
{
    // Task completed within timeout.
    // Consider that the task may have faulted or been canceled.
    // We re-await the task so that any exceptions/cancellation is rethrown.
    await task;

}
else
{
    // timeout/cancellation logic
}

86
Es sollte erwähnt werden, dass Task.Delay zwar vor einer langen laufenden Aufgabe abgeschlossen werden kann, sodass Sie ein Timeout-Szenario bewältigen können, die lange laufende Aufgabe selbst jedoch NICHT abgebrochen wird. WhenAny teilt Ihnen einfach mit, dass eine der an ihn übergebenen Aufgaben abgeschlossen wurde. Sie müssen ein CancellationToken implementieren und die lange laufende Aufgabe selbst abbrechen.
Jeff Schumacher

30
Es kann auch angemerkt werden, dass die Task.DelayAufgabe von einem Systemzeitgeber unterstützt wird, der weiterhin verfolgt wird, bis das Zeitlimit abläuft, unabhängig davon, wie lange es SomeOperationAsyncdauert. Wenn dieses gesamte Code-Snippet häufig in einer engen Schleife ausgeführt wird, verbrauchen Sie Systemressourcen für Timer, bis alle Zeitüberschreitungen auftreten. Die Möglichkeit, dies zu beheben, besteht darin, eine zu übergeben CancellationToken, die Sie nach Abschluss der Freigabe der Timer-Ressource Task.Delay(timeout, cancellationToken)abbrechen SomeOperationAsync.
Andrew Arnott

12
Der Stornierungscode macht viel zu viel Arbeit. Versuchen Sie Folgendes: int timeout = 1000; var cancellationTokenSource = neue CancellationTokenSource (Zeitüberschreitung); var cancellationToken = tokenSource.Token; var task = SomeOperationAsync (CancellationToken); versuche {warte auf Aufgabe; // Code hier für den erfolgreichen Abschluss hinzufügen} catch (OperationCancelledException) {// Code hier für Timeout-Fall
hinzufügen

3
@ilans durch Warten auf Task, wird jede von der Task gespeicherte Ausnahme an diesem Punkt erneut ausgelöst. Dies gibt Ihnen die Möglichkeit, OperationCanceledException(wenn abgebrochen) oder eine andere Ausnahme (falls fehlerhaft) zu fangen .
Andrew Arnott

3
@TomexOu: Die Frage war, wie asynchron auf den Abschluss einer Aufgabe gewartet werden kann . Task.Wait(timeout)würde synchron blockieren statt asynchron warten.
Andrew Arnott

221

Hier ist eine Version der Erweiterungsmethode, die das Abbrechen des Zeitlimits beinhaltet, wenn die ursprüngliche Aufgabe abgeschlossen ist, wie von Andrew Arnott in einem Kommentar zu seiner Antwort vorgeschlagen .

public static async Task<TResult> TimeoutAfter<TResult>(this Task<TResult> task, TimeSpan timeout) {

    using (var timeoutCancellationTokenSource = new CancellationTokenSource()) {

        var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
        if (completedTask == task) {
            timeoutCancellationTokenSource.Cancel();
            return await task;  // Very important in order to propagate exceptions
        } else {
            throw new TimeoutException("The operation has timed out.");
        }
    }
}

8
Gib diesem Mann ein paar Stimmen. Elegante Lösung. Und wenn Ihr Anruf keinen Rückgabetyp hat, stellen Sie sicher, dass Sie nur das TResult entfernen.
Lucas

6
CancellationTokenSource ist verfügbar und sollte in einem usingBlock sein
PeterM

6
@ It'satrap Wenn Sie zweimal auf eine Aufgabe warten, wird das Ergebnis beim zweiten Warten einfach zurückgegeben. Es wird nicht zweimal ausgeführt. Man könnte sagen, dass es gleich ist, task.Result wenn es zweimal ausgeführt wird.
M. Mimpen

7
Wird die ursprüngliche Aufgabe ( task) im Falle einer Zeitüberschreitung weiterhin ausgeführt?
jag

6
Kleinere Verbesserungsmöglichkeit: TimeoutExceptionhat eine geeignete Standardmeldung. Überschreiben mit "Die Operation ist abgelaufen." fügt keinen Wert hinzu und verursacht tatsächlich einige Verwirrung, indem impliziert wird, dass es einen Grund gibt, ihn zu überschreiben.
Edward Brey

49

Sie können Task.WaitAnydamit die erste von mehreren Aufgaben abwarten.

Sie können zwei zusätzliche Aufgaben erstellen (die nach den angegebenen Zeitüberschreitungen abgeschlossen werden) und dann WaitAnywarten, bis die Aufgaben abgeschlossen sind. Wenn die zuerst erledigte Aufgabe Ihre "Arbeits" -Aufgabe ist, sind Sie fertig. Wenn es sich bei der zuerst abgeschlossenen Aufgabe um eine Zeitüberschreitungsaufgabe handelt, können Sie auf die Zeitüberschreitung reagieren (z. B. Stornierung der Anforderung).


1
Ich habe diese Technik von einem MVP gesehen, den ich wirklich respektiere. Sie scheint mir viel sauberer zu sein als die akzeptierte Antwort. Vielleicht würde ein Beispiel helfen, mehr Stimmen zu bekommen! Ich würde mich freiwillig dazu melden, außer ich habe nicht genug Erfahrung mit Aufgaben, um sicher zu sein, dass es hilfreich wäre :)
GrahamMc

3
Ein Thread würde blockiert - aber wenn du damit einverstanden bist, dann kein Problem. Die Lösung, die ich gewählt habe, war die folgende, da keine Threads blockiert sind. Ich habe den Blog-Beitrag gelesen, der wirklich gut war.
JJschk

@JJschk Sie erwähnen, dass Sie die Lösung genommen haben below.... was ist das? basierend auf SO Bestellung?
BozoJoe

und was ist, wenn ich nicht möchte, dass eine langsamere Aufgabe abgebrochen wird? Ich möchte damit umgehen, wenn es
fertig ist,

18

Was ist mit so etwas?

    const int x = 3000;
    const int y = 1000;

    static void Main(string[] args)
    {
        // Your scheduler
        TaskScheduler scheduler = TaskScheduler.Default;

        Task nonblockingTask = new Task(() =>
            {
                CancellationTokenSource source = new CancellationTokenSource();

                Task t1 = new Task(() =>
                    {
                        while (true)
                        {
                            // Do something
                            if (source.IsCancellationRequested)
                                break;
                        }
                    }, source.Token);

                t1.Start(scheduler);

                // Wait for task 1
                bool firstTimeout = t1.Wait(x);

                if (!firstTimeout)
                {
                    // If it hasn't finished at first timeout display message
                    Console.WriteLine("Message to user: the operation hasn't completed yet.");

                    bool secondTimeout = t1.Wait(y);

                    if (!secondTimeout)
                    {
                        source.Cancel();
                        Console.WriteLine("Operation stopped!");
                    }
                }
            });

        nonblockingTask.Start();
        Console.WriteLine("Do whatever you want...");
        Console.ReadLine();
    }

Sie können die Option Task.Wait verwenden, ohne den Hauptthread mit einer anderen Task zu blockieren.


Tatsächlich warten Sie in diesem Beispiel nicht in t1, sondern auf eine obere Aufgabe. Ich werde versuchen, ein detaillierteres Beispiel zu machen.
as-cii

14

Hier ist ein vollständig ausgearbeitetes Beispiel, das auf der am besten bewerteten Antwort basiert:

int timeout = 1000;
var task = SomeOperationAsync();
if (await Task.WhenAny(task, Task.Delay(timeout)) == task) {
    // task completed within timeout
} else { 
    // timeout logic
}

Der Hauptvorteil der Implementierung in dieser Antwort besteht darin, dass Generika hinzugefügt wurden, sodass die Funktion (oder Aufgabe) einen Wert zurückgeben kann. Dies bedeutet, dass jede vorhandene Funktion in eine Timeout-Funktion eingeschlossen werden kann, z.

Vor:

int x = MyFunc();

Nach:

// Throws a TimeoutException if MyFunc takes more than 1 second
int x = TimeoutAfter(MyFunc, TimeSpan.FromSeconds(1));

Dieser Code erfordert .NET 4.5.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TaskTimeout
{
    public static class Program
    {
        /// <summary>
        ///     Demo of how to wrap any function in a timeout.
        /// </summary>
        private static void Main(string[] args)
        {

            // Version without timeout.
            int a = MyFunc();
            Console.Write("Result: {0}\n", a);
            // Version with timeout.
            int b = TimeoutAfter(() => { return MyFunc(); },TimeSpan.FromSeconds(1));
            Console.Write("Result: {0}\n", b);
            // Version with timeout (short version that uses method groups). 
            int c = TimeoutAfter(MyFunc, TimeSpan.FromSeconds(1));
            Console.Write("Result: {0}\n", c);

            // Version that lets you see what happens when a timeout occurs.
            try
            {               
                int d = TimeoutAfter(
                    () =>
                    {
                        Thread.Sleep(TimeSpan.FromSeconds(123));
                        return 42;
                    },
                    TimeSpan.FromSeconds(1));
                Console.Write("Result: {0}\n", d);
            }
            catch (TimeoutException e)
            {
                Console.Write("Exception: {0}\n", e.Message);
            }

            // Version that works on tasks.
            var task = Task.Run(() =>
            {
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return 42;
            });

            // To use async/await, add "await" and remove "GetAwaiter().GetResult()".
            var result = task.TimeoutAfterAsync(TimeSpan.FromSeconds(2)).
                           GetAwaiter().GetResult();

            Console.Write("Result: {0}\n", result);

            Console.Write("[any key to exit]");
            Console.ReadKey();
        }

        public static int MyFunc()
        {
            return 42;
        }

        public static TResult TimeoutAfter<TResult>(
            this Func<TResult> func, TimeSpan timeout)
        {
            var task = Task.Run(func);
            return TimeoutAfterAsync(task, timeout).GetAwaiter().GetResult();
        }

        private static async Task<TResult> TimeoutAfterAsync<TResult>(
            this Task<TResult> task, TimeSpan timeout)
        {
            var result = await Task.WhenAny(task, Task.Delay(timeout));
            if (result == task)
            {
                // Task completed within timeout.
                return task.GetAwaiter().GetResult();
            }
            else
            {
                // Task timed out.
                throw new TimeoutException();
            }
        }
    }
}

Vorsichtsmaßnahmen

Nachdem Sie diese Antwort gegeben haben, ist es im Allgemeinen keine gute Praxis, während des normalen Betriebs Ausnahmen in Ihren Code zu werfen, es sei denn, Sie müssen unbedingt:

  • Jedes Mal, wenn eine Ausnahme ausgelöst wird, ist dies eine extrem schwere Operation.
  • Ausnahmen können Ihren Code um den Faktor 100 oder mehr verlangsamen, wenn sich die Ausnahmen in einer engen Schleife befinden.

Verwenden Sie diesen Code nur, wenn Sie die aufgerufene Funktion absolut nicht ändern können, sodass nach einer bestimmten Zeit eine Zeitüberschreitung auftritt TimeSpan.

Diese Antwort gilt nur für Bibliotheksbibliotheken von Drittanbietern, die Sie einfach nicht umgestalten können, um einen Timeout-Parameter einzuschließen.

Wie schreibe ich robusten Code

Wenn Sie robusten Code schreiben möchten, lautet die allgemeine Regel:

Jede einzelne Operation, die möglicherweise auf unbestimmte Zeit blockiert werden kann, muss eine Zeitüberschreitung aufweisen.

Wenn Sie dies nicht tun diese Regel einhalten, wird Ihr Code möglicherweise einen Vorgang ausführen, der aus irgendeinem Grund fehlschlägt. Dann wird er auf unbestimmte Zeit blockiert und Ihre App ist gerade dauerhaft hängen geblieben.

Wenn nach einiger Zeit eine angemessene Zeitüberschreitung auftritt, bleibt Ihre App extrem lange hängen (z. B. 30 Sekunden). Dann wird entweder ein Fehler angezeigt und der fröhliche Weg fortgesetzt oder es wird erneut versucht.


11

Mit Stephen Clearys ausgezeichneter AsyncEx- Bibliothek können Sie Folgendes tun:

TimeSpan timeout = TimeSpan.FromSeconds(10);

using (var cts = new CancellationTokenSource(timeout))
{
    await myTask.WaitAsync(cts.Token);
}

TaskCanceledException wird im Falle einer Zeitüberschreitung ausgelöst.


10

Dies ist eine leicht verbesserte Version früherer Antworten.

  • Zusätzlich zu Lawrences Antwort wird die ursprüngliche Aufgabe abgebrochen, wenn eine Zeitüberschreitung auftritt.
  • Zusätzlich zu den Antwortvarianten 2 und 3 von sjb können Sie CancellationTokendie ursprüngliche Aufgabe bereitstellen , und wenn eine Zeitüberschreitung auftritt, erhalten Sie TimeoutExceptionstattdessen OperationCanceledException.
async Task<TResult> CancelAfterAsync<TResult>(
    Func<CancellationToken, Task<TResult>> startTask,
    TimeSpan timeout, CancellationToken cancellationToken)
{
    using (var timeoutCancellation = new CancellationTokenSource())
    using (var combinedCancellation = CancellationTokenSource
        .CreateLinkedTokenSource(cancellationToken, timeoutCancellation.Token))
    {
        var originalTask = startTask(combinedCancellation.Token);
        var delayTask = Task.Delay(timeout, timeoutCancellation.Token);
        var completedTask = await Task.WhenAny(originalTask, delayTask);
        // Cancel timeout to stop either task:
        // - Either the original task completed, so we need to cancel the delay task.
        // - Or the timeout expired, so we need to cancel the original task.
        // Canceling will not affect a task, that is already completed.
        timeoutCancellation.Cancel();
        if (completedTask == originalTask)
        {
            // original task completed
            return await originalTask;
        }
        else
        {
            // timeout
            throw new TimeoutException();
        }
    }
}

Verwendungszweck

InnerCallAsyncDie Fertigstellung kann lange dauern. CallAsyncschließt es mit einer Zeitüberschreitung ab.

async Task<int> CallAsync(CancellationToken cancellationToken)
{
    var timeout = TimeSpan.FromMinutes(1);
    int result = await CancelAfterAsync(ct => InnerCallAsync(ct), timeout,
        cancellationToken);
    return result;
}

async Task<int> InnerCallAsync(CancellationToken cancellationToken)
{
    return 42;
}

1
Danke für die Lösung! Scheint , wie Sie passieren sollte timeoutCancellationin delayTask. Derzeit, wenn Sie Stornierung feuern, CancelAfterAsynckann werfen TimeoutExceptionstatt anstatt TaskCanceledException, Ursache delayTaskkann zuerst beendet werden.
AxelUser

@ AxelUser, du hast recht. Ich habe eine Stunde mit einer Reihe von Komponententests gebraucht, um zu verstehen, was passiert ist :) Ich ging davon aus, dass die erste Aufgabe zurückgegeben wird , wenn beide Aufgaben mit WhenAnydemselben Token abgebrochen werden WhenAny. Diese Annahme war falsch. Ich habe die Antwort bearbeitet. Vielen Dank!
Josef Bláha

Es fällt mir schwer herauszufinden, wie ich dies mit einer definierten Task <SomeResult> -Funktion tatsächlich aufrufen kann. Gibt es eine Chance, ein Beispiel dafür zu nennen, wie man es nennt?
Jhaagsma

1
@jhaagsma, Beispiel hinzugefügt!
Josef Bláha

@ JosefBláha Vielen Dank! Ich wickle mich immer noch langsam um die Lambda-Syntax, die mir nicht in den Sinn gekommen wäre - dass das Token durch Übergabe der Lambda-Funktion an die Aufgabe im Hauptteil von CancelAfterAsync übergeben wird. Raffiniert!
Jhaagsma

8

Verwenden Sie einen Timer , um die Nachricht und die automatische Stornierung zu verarbeiten. Wenn die Aufgabe abgeschlossen ist, rufen Sie Dispose für die Timer auf, damit sie niemals ausgelöst werden. Hier ist ein Beispiel; Ändern Sie taskDelay in 500, 1500 oder 2500, um die verschiedenen Fälle anzuzeigen:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        private static Task CreateTaskWithTimeout(
            int xDelay, int yDelay, int taskDelay)
        {
            var cts = new CancellationTokenSource();
            var token = cts.Token;
            var task = Task.Factory.StartNew(() =>
            {
                // Do some work, but fail if cancellation was requested
                token.WaitHandle.WaitOne(taskDelay);
                token.ThrowIfCancellationRequested();
                Console.WriteLine("Task complete");
            });
            var messageTimer = new Timer(state =>
            {
                // Display message at first timeout
                Console.WriteLine("X milliseconds elapsed");
            }, null, xDelay, -1);
            var cancelTimer = new Timer(state =>
            {
                // Display message and cancel task at second timeout
                Console.WriteLine("Y milliseconds elapsed");
                cts.Cancel();
            }
                , null, yDelay, -1);
            task.ContinueWith(t =>
            {
                // Dispose the timers when the task completes
                // This will prevent the message from being displayed
                // if the task completes before the timeout
                messageTimer.Dispose();
                cancelTimer.Dispose();
            });
            return task;
        }

        static void Main(string[] args)
        {
            var task = CreateTaskWithTimeout(1000, 2000, 2500);
            // The task has been started and will display a message after
            // one timeout and then cancel itself after the second
            // You can add continuations to the task
            // or wait for the result as needed
            try
            {
                task.Wait();
                Console.WriteLine("Done waiting for task");
            }
            catch (AggregateException ex)
            {
                Console.WriteLine("Error waiting for task:");
                foreach (var e in ex.InnerExceptions)
                {
                    Console.WriteLine(e);
                }
            }
        }
    }
}

Außerdem bietet Async CTP eine TaskEx.Delay-Methode, mit der die Timer in Aufgaben für Sie eingeschlossen werden. Dies kann Ihnen mehr Kontrolle geben, um beispielsweise den TaskScheduler für die Fortsetzung festzulegen, wenn der Timer ausgelöst wird.

private static Task CreateTaskWithTimeout(
    int xDelay, int yDelay, int taskDelay)
{
    var cts = new CancellationTokenSource();
    var token = cts.Token;
    var task = Task.Factory.StartNew(() =>
    {
        // Do some work, but fail if cancellation was requested
        token.WaitHandle.WaitOne(taskDelay);
        token.ThrowIfCancellationRequested();
        Console.WriteLine("Task complete");
    });

    var timerCts = new CancellationTokenSource();

    var messageTask = TaskEx.Delay(xDelay, timerCts.Token);
    messageTask.ContinueWith(t =>
    {
        // Display message at first timeout
        Console.WriteLine("X milliseconds elapsed");
    }, TaskContinuationOptions.OnlyOnRanToCompletion);

    var cancelTask = TaskEx.Delay(yDelay, timerCts.Token);
    cancelTask.ContinueWith(t =>
    {
        // Display message and cancel task at second timeout
        Console.WriteLine("Y milliseconds elapsed");
        cts.Cancel();
    }, TaskContinuationOptions.OnlyOnRanToCompletion);

    task.ContinueWith(t =>
    {
        timerCts.Cancel();
    });

    return task;
}

Er möchte nicht, dass der aktuelle Thread blockiert wird, das heißt, nein task.Wait().
Cheng Chen

@Danny: Das war nur, um das Beispiel zu vervollständigen. Nach dem ContinueWith können Sie zurückkehren und die Aufgabe ausführen lassen. Ich werde meine Antwort aktualisieren, um das klarer zu machen.
Quartiermeister

2
@dtb: Was ist, wenn Sie t1 zu einer Task <Task <Ergebnis >> machen und dann TaskExtensions.Unwrap aufrufen? Sie können t2 von Ihrem inneren Lambda zurückgeben und anschließend der ausgepackten Aufgabe Fortsetzungen hinzufügen.
Quartiermeister

Genial! Das löst mein Problem perfekt. Vielen Dank! Ich denke, ich werde mit der von @ AS-CII vorgeschlagenen Lösung fortfahren, obwohl ich wünschte, ich könnte Ihre Antwort auch akzeptieren, wenn ich TaskExtensions vorschlage.
dtb

6

Eine andere Möglichkeit, dieses Problem zu lösen, ist die Verwendung von Reactive Extensions:

public static Task TimeoutAfter(this Task task, TimeSpan timeout, IScheduler scheduler)
{
        return task.ToObservable().Timeout(timeout, scheduler).ToTask();
}

Testen Sie oben mit dem folgenden Code in Ihrem Unit-Test, es funktioniert für mich

TestScheduler scheduler = new TestScheduler();
Task task = Task.Run(() =>
                {
                    int i = 0;
                    while (i < 5)
                    {
                        Console.WriteLine(i);
                        i++;
                        Thread.Sleep(1000);
                    }
                })
                .TimeoutAfter(TimeSpan.FromSeconds(5), scheduler)
                .ContinueWith(t => { }, TaskContinuationOptions.OnlyOnFaulted);

scheduler.AdvanceBy(TimeSpan.FromSeconds(6).Ticks);

Möglicherweise benötigen Sie den folgenden Namespace:

using System.Threading.Tasks;
using System.Reactive.Subjects;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using Microsoft.Reactive.Testing;
using System.Threading;
using System.Reactive.Concurrency;

4

Eine generische Version der obigen Antwort von @ Kevan unter Verwendung von Reactive Extensions.

public static Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout, IScheduler scheduler)
{
    return task.ToObservable().Timeout(timeout, scheduler).ToTask();
}

Mit optionalem Scheduler:

public static Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout, Scheduler scheduler = null)
{
    return scheduler is null 
       ? task.ToObservable().Timeout(timeout).ToTask() 
       : task.ToObservable().Timeout(timeout, scheduler).ToTask();
}

Übrigens: Wenn ein Timeout auftritt, wird eine Timeout-Ausnahme ausgelöst


0

Wenn Sie die Aufgabe mit einer BlockingCollection planen, kann der Produzent die möglicherweise lange laufende Aufgabe ausführen, und der Verbraucher kann die TryTake-Methode verwenden, in die ein Timeout- und Abbruchtoken integriert ist.


Ich müsste etwas aufschreiben (ich möchte hier keinen proprietären Code einfügen), aber das Szenario ist wie folgt. Der Produzent ist der Code, der die Methode ausführt, bei der eine Zeitüberschreitung auftreten kann, und die Ergebnisse nach Abschluss in die Warteschlange stellt. Der Verbraucher ruft trytake () mit Zeitüberschreitung auf und erhält das Token bei Zeitüberschreitung. Sowohl Produzent als auch Konsument sind Hintergrundaufgaben und zeigen dem Benutzer bei Bedarf mithilfe des UI-Thread-Dispatchers eine Nachricht an.
Kns98

0

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

Und obwohl Joe Hoags Crafting a Task.TimeoutAfter-Methode in MSDN-Blogs inspirierend war, war ich es ein wenig leid, sie zu verwendenTimeoutException aus dem gleichen Grund wie oben für die Flusskontrolle zu verwenden, da Timeouts häufiger erwartet werden als nicht.

Also habe ich mich dafür entschieden, das auch die im Blog erwähnten Optimierungen behandelt:

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 folgender:

var receivingTask = conn.ReceiveAsync(ct);

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

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

0

Einige Varianten von Andrew Arnotts Antwort:

  1. Wenn Sie auf eine vorhandene Aufgabe warten und herausfinden möchten, ob sie abgeschlossen oder abgelaufen ist, sie jedoch nicht abbrechen möchten, wenn das Zeitlimit auftritt:

    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 das Zeitlimit 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 abbrechen möchten, wenn eine Zeitüberschreitung auftritt:

    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
    }

Ein weiterer Kommentar: Diese Versionen brechen den Timer ab, wenn das Timeout nicht auftritt, sodass sich bei mehreren Anrufen keine Timer häufen.

sjb


0

Ich fasse die Ideen einiger anderer Antworten hier und diese Antwort in einem anderen Thread zu einer Erweiterungsmethode im Try-Stil zusammen. Dies hat einen Vorteil, wenn Sie eine Erweiterungsmethode wünschen und dennoch eine Ausnahme bei Zeitüberschreitung vermeiden möchten.

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

-3

Tun Sie dies auf keinen Fall, aber es ist eine Option, wenn ... mir kein gültiger Grund einfällt.

((CancellationTokenSource)cancellationToken.GetType().GetField("m_source",
    System.Reflection.BindingFlags.NonPublic |
    System.Reflection.BindingFlags.Instance
).GetValue(cancellationToken)).Cancel();
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.