wake-up-neo.net

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

Ich möchte warten, bis ein Task <T> mit einigen speziellen Regeln abgeschlossen ist: Wenn es nach X Millisekunden nicht abgeschlossen ist, möchte ich dem Benutzer eine Nachricht anzeigen. .... Wenn ja nach Y Millisekunden nicht abgeschlossen, ich möchte automatisch Stornierung beantragen .

Ich kann Task.ContinueWith verwenden, um asynchron zu warten, bis die Aufgabe abgeschlossen ist (dh, eine Aktion auszuführen, die ausgeführt wird, wenn die Aufgabe abgeschlossen ist), aber dies erlaubt keine Angabe eines Zeitlimits Task.Wait synchron warten, bis die Task mit einem Timeout abgeschlossen ist, aber das blockiert meinen Thread . Wie kann ich asynchron warten, bis der Task mit einem Timeout abgeschlossen ist?

302
dtb

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 Blogeintrag "Crafting a Task.TimeoutAfter Method" (vom MS Parallel Library-Team) mit weiteren Informationen zu dieser Art von Dingen .

Addition: Auf Anfrage eines Kommentars zu meiner Antwort, hier eine erweiterte Lösung, die die Stornierungsabwicklung beinhaltet. Wenn Sie die Aufgabe und den Zeitgeber abbrechen, bedeutet dies, dass der Code auf verschiedene Arten und Weise aufgehoben werden kann, und Sie sollten sicher sein, darauf zu testen und sicher zu sein, dass Sie alle Aufgaben ordnungsgemäß erledigen. Lassen Sie verschiedene Kombinationen nicht dem Zufall zu 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
}
460
Andrew Arnott

Hier ist eine Version der Erweiterungsmethode, die die Annullierung des Timeouts 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.");
        }
    }
}
148

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

Sie können zwei zusätzliche Aufgaben erstellen (die nach den angegebenen Zeitüberschreitungen ausgeführt werden) und dann mit WaitAny auf den ersten Abschluss warten. Wenn die zuerst abgeschlossene Aufgabe Ihre "Arbeitsaufgabe" ist, sind Sie fertig. Wenn es sich bei der zuerst abgeschlossenen Aufgabe um eine Zeitüberschreitung handelt, können Sie auf die Zeitüberschreitung reagieren (z. B. Abbruch der Anforderung).

44
Tomas Petricek

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 Task.Wait-Option verwenden, ohne den Haupt-Thread mit einer anderen Task zu blockieren.

17
as-cii

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 Zeitüberschreitungsfunktion eingeschlossen werden kann, z.

Vorher:

int x = MyFunc();

After:

// 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 nicht eine gute Praxis, im normalen Betrieb Ausnahmen in Ihren Code zu werfen, es sei denn, Sie müssen unbedingt:

  • Jedes Mal, wenn eine Ausnahme geworfen 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 einem bestimmten TimeSpan eine Zeitüberschreitung auftritt.

Diese Antwort ist wirklich nur anwendbar, wenn es sich um Bibliotheken von Drittanbietern handelt, 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 wie folgt:

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

Wenn Sie nicht tun diese Regel einhalten, wird Ihr Code irgendwann einen Vorgang treffen, der aus irgendeinem Grund fehlschlägt, dann wird er auf unbestimmte Zeit blockiert und Ihre App hat sich gerade endgültig aufgehängt.

Wenn nach einiger Zeit eine angemessene Zeitüberschreitung auftritt, bleibt Ihre App extrem lange hängen (z. B. 30 Sekunden), zeigt dann entweder einen Fehler an und setzt den fröhlichen Vorgang fort oder versucht es erneut.

13
Contango

Verwenden Sie einen Timer , um die Nachricht und den automatischen Abbruch zu bearbeiten. 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 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);
                }
            }
        }
    }
}

Das Async CTP bietet auch eine TaskEx.Delay-Methode, mit der die Timer für Sie in Tasks eingeschlossen werden. Dadurch erhalten Sie mehr Kontrolle, wenn Sie den TaskScheduler für die Fortsetzung einstellen, 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;
}
8
Quartermeister

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 den obigen Code in Ihrem Gerätetest, 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;
6
Kevan

Mit Stephen Clearys exzellenter 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 eines Timeouts geworfen.

6
Cocowalla

Eine generische Version von @ Kevans Antwort mit 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 == null 
       ? task.ToObservable().Timeout(timeout).ToTask() 
       : task.ToObservable().Timeout(timeout, scheduler).ToTask();
}

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

2
Jasper H Bojsen

Einige Varianten der Antwort von Andrew Arnott: 

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

    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 der Timeout 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 bei einem Timeout 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
    }
    

Ein weiterer Kommentar, diese Versionen stornieren den Timer, wenn das Zeitlimit nicht auftritt. Mehrere Aufrufe führen nicht dazu, dass sich die Timer stauen. 

sjb

0
sjb-sjb

Wenn Sie eine BlockingCollection zum Planen der Aufgabe verwenden, kann der Produzent die möglicherweise lang laufende Aufgabe ausführen, und der Consumer kann die TryTake-Methode verwenden, in die Timeout- und Abbruchtoken integriert sind.

0
kns98