Frage

Ich möchte für einen warten Aufgabe mit einigen speziellen Regeln zu vervollständigen: Wenn es nach X Millisekunden nicht abgeschlossen hat, mag ich den Benutzer eine Nachricht angezeigt werden soll. Und wenn es nach Y Millisekunden nicht abgeschlossen hat, möchte ich automatisch Anfrage Stornierung .

kann ich Task.ContinueWith asynchron für die Aufgabe abzuschließen warten (dh Zeitplan eine Aktion, die ausgeführt werden, wenn die Aufgabe abgeschlossen ist), aber das darf nicht ein Timeout angeben. Ich kann benutzen Task.Wait synchron zu warten, bis die Aufgabe mit einem Timeout zu beenden, aber das Blöcke mein Thread. Wie kann ich asynchron für die Aufgabe warten mit einem Timeout zu vervollständigen?

War es hilfreich?

Lösung

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ßer Blog-Eintrag „Crafting eine Aufgabe .TimeoutAfter Method“(von MS-Bibliothek Team Parallel) mit mehr Informationen über diese Art der Sache .

Addition : auf Antrag eines Kommentars auf meine Antwort, hier ist eine erweiterte Lösung, die Stornierung Handhabung beinhaltet. Beachten Sie, dass Löschung der Aufgabe vorbei, und der Zeit bedeutet, dass es mehrere Möglichkeiten gibt Stornierung in Ihrem Code erfahren werden kann, und Sie sollten sicher zu Test sein und sicher sein, Sie richtig alle von ihnen zu behandeln. Lassen Sie keine verschiedenen Kombinationen Chance und hoffen, dass Ihr Computer das Richtige zur Laufzeit.

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
}

Andere Tipps

Hier ist eine Erweiterung Methode Version, die Aufhebung des Timeout enthält, wenn die ursprüngliche Aufgabe abgeschlossen ist, wie durch Andrew Arnott in einem Kommentar 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.");
        }
    }
}

Sie können Task.WaitAny verwenden, um die ersten von mehreren Aufgaben zu warten.

Sie könnten zwei zusätzliche Aufgaben erstellen (die komplette nach dem angegebenen Timeouts) und dann zur Verwendung WaitAny warten je nachdem, welche zuerst abgeschlossen ist. Wenn die Aufgabe, die zuerst Ihre „Arbeit“ Aufgabe abgeschlossen ist, dann sind Sie fertig. Wenn die Aufgabe, die erste abgeschlossen eine Timeout Aufgabe ist, dann können Sie mit dem Timeout reagieren (z Anfrage Stornierung).

Was ist so etwas wie das?

    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 ohne Haupt-Thread blockiert eine andere Aufgabe verwendet wird.

Hier ist ein voll gearbeitet Beispiel auf der Grundlage des oben Antwort gestimmt, das ist:

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 Umsetzung in dieser Antwort ist, dass Generika hinzugefügt wurde, so dass die Funktion (oder Aufgabe) einen Wert zurückgeben kann. Dies bedeutet, dass jede bestehende Funktion kann in einer Timeout-Funktion eingewickelt werden, z.

Vorher:

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

Caveats

Sie diese Antwort gegeben haben, die allgemein nicht eine gute Praxis, Ausnahmen in Ihrem Code während des normalen Betriebs geworfen zu haben, wenn Sie absolut haben:

  • Jedes Mal, wenn eine Ausnahme ausgelöst wird, sein extrem Schwerer Betrieb,
  • Ausnahmen Sie den Code um einen Faktor von 100 verlangsamen oder mehr, wenn die Ausnahmen in einer engen Schleife sind.

Nur diesen Code verwenden, wenn Sie absolut nicht die Funktion verändern können Sie so es wird nach einem bestimmten TimeSpan anrufen.

Diese Antwort ist wirklich nur dann anwendbar, wenn mit 3rd-Party-Bibliothek Bibliotheken zu tun, dass man einfach nicht refactor einen Timeout-Parameter enthalten.

Wie robust Code schreiben

Wenn Sie robusten Code schreiben wollen, ist die allgemeine Regel folgt aus:

  

Jede einzelne Operation, die möglicherweise auf unbestimmte Zeit blockieren könnte, muss ein Timeout hat.

Wenn Sie nicht diese Regel befolgen, Ihr Code schlagen schließlich eine Operation, die aus irgendeinem Grund ausfällt, dann wird es auf unbestimmte Zeit sperren und Ihre App hing nur fest.

Wenn es eine vernünftige Zeitspanne, nach einiger Zeit war, dann wird Ihre App für einige extrem viel Zeit hängen würde (zum Beispiel 30 Sekunden), dann wäre es entweder einen Fehler anzeigen und weiterhin auf seine fröhliche Art und Weise, oder erneut versuchen können.

Verwenden Sie einen Timer um die Nachricht zu verarbeiten und automatische Löschung. Wenn die Aufgabe abgeschlossen ist, rufen Sie Dispose auf den Timer so, dass sie nie Feuer. Hier ist ein Beispiel; Änderung taskDelay bis 500, 1500, oder 2500 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 ein TaskEx.Delay Verfahren, dass wird der Timer in Aufgaben für Sie wickeln. Dies kann Ihnen mehr Kontrolle Dinge wie setzen die Taskscheduler für die Fortsetzung, wenn die Timer Feuer zu tun.

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

Mit Stephen Cleary ausgezeichneten 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 Fall eines Timeout geworfen werden.

Eine weitere Möglichkeit, dieses Problem zu lösen, wird mit Reactive Extensions:

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

Test oben oben unten Code in Ihrem Unit-Test verwenden, 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);

Sie können die folgenden Namespace müssen:

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;

Eine generische Version von @ Kevan Antwort oben 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 dem optionalen 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 geschieht, wird ein Timeout Ausnahme ausgelöst werden

Wenn Sie eine Blocking verwenden, um die Aufgabe zu planen, kann der Hersteller läuft die potenziell lange laufende Aufgabe und die Verbraucher können die TryTake Methode verwenden, die Timeout hat und Stornierungs Token eingebaut.

Dies ist eine leicht verbesserte Version der bisherigen Antwort.

  • Neben Lawrence Antwort , bricht es die ursprüngliche Aufgabe, wenn Timeout auftritt.
  • In addtion sjb Antwort Varianten 2 und 3 , können Sie CancellationToken für die ursprüngliche Aufgabe zur Verfügung stellen, und wenn Timeout auftritt Sie erhalten TimeoutException statt 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, combinedCancellation.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();
        }
    }
}

Einige Varianten von Andrew Arnott Antwort:

  1. Wenn Sie eine vorhandene Aufgabe warten wollen und herausfinden, ob es abgeschlossen oder Zeitüberschreitung, aber nicht wollen, es zu kündigen, wenn der Timeout 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 gestartet werden sollen und die Arbeit abbrechen, wenn das 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 eine Aufgabe haben bereits erstellt, dass Sie abbrechen möchten, wenn ein Timeout 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, werden diese Versionen den Timer beenden, wenn das Timeout tritt nicht auf, so dass mehrere Anrufe nicht Ursache Timer zu stapeln.

sjb

Ich spürte die Task.Delay() Aufgabe und CancellationTokenSource in den anderen Antworten ein wenig zu viel für meinen Anwendungsfall in einer eng ish-Networking-Schleife.

zwar Joe Hoags Crafting eine Task.TimeoutAfter Methode auf MSDN Blogs inspiriert wurde, war ich ein wenig müde TimeoutException für die Ablaufsteuerung aus dem gleichen Grund der Verwendung wie oben, weil Timeouts häufiger zu erwarten sind als nicht.

Also habe ich damit ging, die Griffe auch die Optimierungen im Blog erwähnt:

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 Anwendungsfall ist als solche:

var receivingTask = conn.ReceiveAsync(ct);

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

// Read and do something with data
var data = await receivingTask;
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top