web-dev-qa-db-fra.com

Attendez de manière asynchrone que la tâche <T> se termine avec un délai

Je souhaite attendre que Task <T> se termine avec certaines règles spéciales: Si elle n'est pas terminée au bout de X millisecondes, je souhaite afficher un message à l'utilisateur . Et si n'a pas terminé au bout de Y millisecondes, je souhaite automatiquement demander l'annulation .

Je peux utiliser Task.ContinueWith pour attendre de manière asynchrone la fin de la tâche (c.-à-d. Planifier l'exécution d'une action lorsque la tâche est terminée), mais cela ne permet pas de spécifier un délai d'expiration ..__ Task.Wait attend synchroniquement que la tâche se termine avec un délai d'expiration, mais cela bloque mon thread . Comment puis-je attendre de manière asynchrone que la tâche se termine avec un délai d'expiration?

302
dtb

Que dis-tu de ça:

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 excellent article de blog "Crafting a Task.TimeoutAfter Method" (de l’équipe de MS Parallel Library) avec plus d’informations sur ce genre de choses .

Addition: à la demande d'un commentaire sur ma réponse, voici une solution étendue incluant la gestion des annulations. Notez que l'annulation de la tâche et du minuteur signifie que votre code peut être annulé de différentes manières. Vous devez donc être certain de les tester et de vous assurer de les gérer correctement. Ne laissez pas au hasard diverses combinaisons et espérez que votre ordinateur fera le bon choix au moment de 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
}
460
Andrew Arnott

Voici une version de la méthode d'extension qui intègre l'annulation du délai d'expiration lorsque la tâche d'origine est terminée, comme l'a suggéré 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.");
        }
    }
}
148
Lawrence Johnston

Vous pouvez utiliser Task.WaitAny pour attendre la première de plusieurs tâches.

Vous pouvez créer deux tâches supplémentaires (qui se terminent après les délais impartis), puis utiliser WaitAny pour attendre la première de ces tâches. Si la tâche qui s'est terminée en premier est votre tâche "de travail", vous avez terminé. Si la tâche qui s'est terminée en premier est une tâche de délai d'attente, vous pouvez réagir au délai d'attente (par exemple, l'annulation de la demande).

44
Tomas Petricek

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 le thread principal à l'aide d'une autre tâche.

17
as-cii

Voici un exemple entièrement travaillé basé sur la réponse la plus votée, à savoir:

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 l'implémentation dans cette réponse est que des génériques ont été ajoutés, de sorte que la fonction (ou la tâche) peut renvoyer une valeur. Cela signifie que toute fonction existante peut être encapsulée 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 .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();
            }
        }
    }
}

Mises en garde

Après avoir donné cette réponse, il s’agit généralement d’une non bonne pratique de générer des exceptions dans votre code, sauf si vous devez absolument: 

  • Chaque fois qu’une exception est levée, c’est une opération extrêmement lourde, 
  • Les exceptions peuvent ralentir votre code d'un facteur 100 ou plus si les exceptions sont dans une boucle serrée.

N'utilisez ce code que si vous ne pouvez absolument pas modifier la fonction que vous appelez afin qu'elle expire après une TimeSpan spécifique.

Cette réponse ne s'applique en réalité qu'aux bibliothèques de bibliothèques tierces que vous ne pouvez tout simplement pas refactoriser pour inclure un paramètre de délai d'expiration.

Comment écrire du code robuste

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

Toute opération pouvant potentiellement bloquer indéfiniment doit avoir un délai d'expiration.

Si vous faites pas respectez cette règle, votre code finira par frapper une opération qui échoue pour une raison quelconque, puis il se bloque indéfiniment et votre application est suspendue de façon permanente. 

S'il y avait un délai raisonnable après un certain temps, votre application resterait bloquée pendant un laps de temps extrême (par exemple, 30 secondes), puis elle afficherait une erreur et continuerait sur sa lancée, ou réessayer.

13
Contango

Utilisez un Minuteur pour gérer le message et l’annulation automatique. Lorsque la tâche est terminée, appelez Dispose sur les minuteries pour qu’elles ne se déclenchent jamais. Voici un exemple; changez taskDelay en 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);
                }
            }
        }
    }
}

De plus, le Async CTP fournit une méthode TaskEx.Delay qui encapsulera les minuteurs dans des tâches. Cela peut vous donner plus de contrôle pour faire des choses comme définir le TaskScheduler pour la suite lorsque le minuteur est déclenché. 

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

Une autre façon de résoudre ce problème consiste à utiliser les extensions réactives:

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

Testez ci-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 aurez peut-être besoin de 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;
6
Kevan

En utilisant l'excellente bibliothèque AsyncEx de Stephen Cleary, vous pouvez effectuer les tâches suivantes:

TimeSpan timeout = TimeSpan.FromSeconds(10);

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

TaskCanceledException sera lancé en cas de dépassement du délai d'attente.

6
Cocowalla

Une version générique de la réponse de @ Kevan ci-dessus avec Reactive Extensions.

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

Avec planificateur optionnel:

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: Quand un timeout arrive, une exception de timeout sera levée

2
Jasper H Bojsen

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

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

    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 souhaitez démarrer une tâche et annuler le travail si le délai est dépassé: 

    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 déjà créé une tâche que vous souhaitez annuler en cas d'expiration du délai: 

    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
    }
    

Autre commentaire, ces versions annuleront le chronomètre si le délai d'attente ne se produit pas. Par conséquent, plusieurs appels n'entraîneront pas une accumulation de minuteurs. 

sjb

0
sjb-sjb

Si vous utilisez une BlockingCollection pour planifier la tâche, le producteur peut exécuter la tâche potentiellement longue et le consommateur peut utiliser la méthode TryTake qui contient un jeton de délai d'attente et d'annulation.

0
kns98