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?

26voto

Cocowalla Punkte 12746

Mit Stephen Clearys ausgezeichnetem AsyncEx Bibliothek, können Sie 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.

18voto

Contango Punkte 70203

Hier ist ein vollständig ausgearbeitetes Beispiel, das auf der am häufigsten gewählten Antwort basiert, die lautet:

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 ist, dass Generika hinzugefügt wurden, so dass die Funktion (oder Aufgabe) einen Wert zurückgeben kann. Dies bedeutet, dass jede bestehende Funktion in eine Timeout-Funktion verpackt werden kann, z. B.:

Vorher:

int x = MyFunc();

Danach:

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

Vorbehalte

Nachdem diese Antwort gegeben wurde, ist es im Allgemeinen no Es ist keine gute Praxis, während des normalen Betriebs Ausnahmen in Ihrem Code auszulösen, es sei denn, Sie müssen dies unbedingt tun:

  • Jedes Mal, wenn eine Ausnahme ausgelöst wird, ist dies ein extrem schwerer Vorgang,
  • 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 Funktion, die Sie aufrufen, absolut nicht ändern können, damit sie nach einer bestimmten TimeSpan .

Diese Antwort ist wirklich nur anwendbar, wenn Sie mit Bibliotheken von Drittanbietern zu tun haben, die Sie einfach nicht so umgestalten können, dass sie einen Timeout-Parameter enthalten.

Wie man robusten Code schreibt

Wenn Sie einen robusten Code schreiben wollen, gilt die folgende Regel:

Jede einzelne Operation, die potenziell unbegrenzt blockiert werden kann, muss eine Zeitüberschreitung haben.

もし、あなたが nicht Wenn Sie diese Regel beachten, wird Ihr Code irgendwann auf einen Vorgang stoßen, der aus irgendeinem Grund fehlschlägt, dann wird er auf unbestimmte Zeit blockiert, und Ihre Anwendung ist einfach dauerhaft hängen geblieben.

Wenn es nach einer gewissen Zeit eine vernünftige Zeitüberschreitung gäbe, würde Ihre Anwendung für eine extreme Zeitspanne (z. B. 30 Sekunden) hängen bleiben und dann entweder eine Fehlermeldung anzeigen und ihren fröhlichen Weg fortsetzen, oder es erneut versuchen.

17voto

as-cii Punkte 12459

Wie wäre es mit etwas wie diesem?

    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 zu blockieren, indem Sie eine andere Task verwenden.

9voto

Quartermeister Punkte 55099

Verwenden Sie eine Zeitschaltuhr um die Nachricht und die automatische Stornierung zu bearbeiten. Wenn der Task abgeschlossen ist, rufen Sie Dispose für die Timer auf, damit sie nicht mehr ausgelöst werden. Hier ist ein Beispiel; ändern Sie taskDelay auf 500, 1500 oder 2500, um die verschiedenen Fälle zu sehen:

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

Auch die Async CTP bietet eine TaskEx.Delay-Methode, die die Zeitgeber in Aufgaben für Sie verpackt. Dadurch haben Sie mehr Kontrolle über Dinge wie das Festlegen des TaskSchedulers für die Fortsetzung, 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;
}

9voto

Armand Punkte 211

Mit .Net 6 (Preview 7 zum Zeitpunkt dieser Antwort) ist es möglich, die neue WaitAsync(TimeSpan, CancellationToken) die diesem besonderen Bedarf gerecht wird. Wenn Sie .Net6 verwenden können, wird diese Version außerdem als optimiert beschrieben, wenn wir sie mit der Mehrheit der in diesem Beitrag vorgeschlagenen guten Lösungen vergleichen.

(Danke für alle Teilnehmer, denn ich habe Ihre Lösung jahrelang verwendet)

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