Question

Je veux attendre un Tâche pour compléter avec des règles spéciales: Si elle n'a pas terminé après X millisecondes, je veux afficher un message à l'utilisateur. Et si elle n'a pas terminé après millisecondes Y, je veux automatiquement demande l'annulation .

Je peux utiliser Task.ContinueWith pour attendre la manière asynchrone tâche complète (calendrier une action à exécuter lorsque la tâche est terminée), mais cela ne permet pas de spécifier un délai d'attente. Je peux utiliser Task.Wait attendre pour la tâche synchrone pour terminer avec un délai d'attente, mais blocs mon fil. Comment puis-je attendre la manière asynchrone tâche à accomplir avec un délai d'attente?

Était-ce utile?

La solution

Que diriez-vous ceci:

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

Et voici un billet de blog « Crafting une tâche .TimeoutAfter Méthode »(à partir de MS Parallel Library équipe) avec plus d'informations sur ce genre de chose .

Addition : à la demande d'un commentaire sur ma réponse, voici une solution étendue qui comprend le traitement d'annulation. Notez que le passage à l'annulation de la tâche et les moyens de minuterie qu'il ya plusieurs façons d'annulation peut être expérimenté dans votre code, et vous devriez être sûr de tester et être sûr que vous gérez correctement tous. Ne pas laisser au hasard diverses combinaisons et espérons que votre ordinateur fait la bonne chose à l'exécution.

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
}

Autres conseils

Voici une version de méthode d'extension qui intègre l'annulation du délai d'attente lorsque les finalise la tâche d'origine, comme suggéré par Andrew Arnott dans un commentaire à sa réponse .

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

Vous pouvez utiliser Task.WaitAny attendre la première des tâches multiples.

Vous pouvez créer deux tâches supplémentaires (qui complète après les délais d'attente spécifiés), puis utilisez WaitAny d'attendre selon complète la première. Si la tâche qui a terminé premier est votre tâche « de travail », alors vous avez terminé. Si la tâche qui a terminé la première est une tâche de délai d'attente, vous pouvez alors réagissez au délai d'attente (par exemple la demande d'annulation).

Qu'en est-il quelque chose comme ça?

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

Vous pouvez utiliser l'option Task.Wait sans bloquer thread principal en utilisant une autre tâche.

Voici un exemple entièrement travaillé en fonction de la réponse voté en haut, ce qui est:

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

Le principal avantage de la mise en œuvre dans cette réponse est que les médicaments génériques ont été ajoutés, de sorte que la fonction (ou tâche) peut renvoyer une valeur. Cela signifie que toute fonction existante peut être enveloppées dans une fonction de délai d'attente, par exemple:.

Avant:

int x = MyFunc();

Après:

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

Ce code nécessite 4,5 .NET.

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

Avertissements

Après avoir donné cette réponse, le général pas une bonne pratique d'avoir des exceptions lancées dans votre code pendant le fonctionnement normal, à moins que vous devez absolument:

  • Chaque fois qu'une exception est levée, son une opération extrêmement lourd,
  • Des exceptions peuvent ralentir votre code par un facteur de 100 ou plus si les exceptions sont dans une boucle serrée.

Utilisez uniquement ce code si vous ne pouvez absolument pas modifier la fonction que vous appelez ainsi, il temps après une TimeSpan spécifique.

Cette réponse est vraiment applicable uniquement lorsqu'ils traitent avec les bibliothèques de la bibliothèque 3e partie que vous ne pouvez pas refactoring d'inclure un paramètre de délai d'attente.

Comment écrire un code robuste

Si vous voulez écrire un code robuste, la règle générale est la suivante:

  

Chaque seule opération qui pourrait potentiellement bloquer indéfiniment, doit avoir un délai d'attente.

Si vous ne pas respect de cette règle, votre code finira par frapper une opération qui échoue pour une raison quelconque, il va bloquer indéfiniment, et votre application vient en permanence accroché.

S'il y avait un délai raisonnable après un certain temps, votre application pendrait pour une certaine quantité d'extrême du temps (par exemple 30 secondes), il affichera alors soit une erreur et continuer son bonhomme de chemin, ou nouvelle tentative.

Utilisez un minuterie pour gérer le message et annulation automatique. Lorsque le finalise la tâche, appeler Dispose sur les minuteries afin qu'ils ne seront jamais le feu. Voici un exemple; changement taskDelay à 500, 1500 ou 2500 pour voir les différents cas:

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

En outre, le Async CTP fournit un procédé qui TaskEx.Delay enveloppera les minuteries dans les tâches pour vous. Cela peut vous donner plus de contrôle à faire des choses comme mettre la TaskScheduler pour la suite lorsque les feux de la minuterie.

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

L'utilisation de l'excellent Stephen Cleary AsyncEx bibliothèque, vous pouvez faire:

TimeSpan timeout = TimeSpan.FromSeconds(10);

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

TaskCanceledException sera jeté dans le cas d'un délai d'attente.

Une autre façon de résoudre ce problème utilise Reactive Extensions:

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

test au-dessus en utilisant le code ci-dessous dans votre test unitaire, cela fonctionne pour moi

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

Vous pouvez avoir besoin l'espace de noms suivant:

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;

Une version générique de la réponse de @ Kevan ci-dessus avec les extensions réactives.

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

Avec option Planificateur:

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: Lorsqu'un délai d'attente se produit, une exception de délai d'attente sera levée

Si vous utilisez un BlockingCollection pour planifier la tâche, le producteur peut exécuter la tâche en cours d'exécution potentiellement long et le consommateur peut utiliser la méthode TryTake qui a délai et jeton annulation intégré.

Ceci est une version légèrement améliorée de réponses précédentes.

  • En plus de réponse de Laurent , il annule la tâche d'origine lorsque délai a expiré.
  • variantes de réponse sjb 2 et 3 , vous pouvez fournir CancellationToken pour la tâche d'origine, et quand temporisation se produit , vous TimeoutException au lieu de 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();
        }
    }
}

Quelques variantes de la réponse d'Andrew Arnott:

  1. Si vous voulez attendre une tâche existante et savoir si elle a terminé ou expiré, mais ne voulez pas l'annuler si le délai d'attente se produit:

    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. Si vous voulez commencer une tâche de travail et annuler le travail si le délai d'attente se produit:

    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. Si vous avez une tâche déjà créé que vous souhaitez annuler si un délai d'attente se produit:

    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
    }
    

Un autre commentaire, ces versions annulera la minuterie si le délai d'attente ne se produit pas, si plusieurs appels ne seront pas la cause des minuteries pour accumuler.

sjb

je sentais la tâche Task.Delay() et CancellationTokenSource dans les autres réponses un peu beaucoup pour mon cas d'utilisation dans une boucle de réseau-ish serré.

Et bien que Joe Hoag de l'élaboration d'une méthode Task.TimeoutAfter sur les blogs MSDN a été source d'inspiration, je suis un peu las d'utiliser TimeoutException pour le contrôle de flux pour la même raison que ci-dessus, parce que les délais d'attente sont plus souvent qu'autrement prévu.

Alors je suis allé avec cela, qui gère également les optimisations mentionnées dans le blog:

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

case Un exemple d'utilisation est en tant que tel:

var receivingTask = conn.ReceiveAsync(ct);

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

// Read and do something with data
var data = await receivingTask;
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top