Attendre de manière asynchrone que la tâche <T> se termine avec le délai d'expiration

388

Je veux attendre qu'une tâche <T> se termine avec quelques règles spéciales: si elle ne s'est pas terminée après X millisecondes, je veux afficher un message à l'utilisateur. Et si elle ne s'est pas terminée après Y millisecondes, je souhaite demander automatiquement l' annulation .

Je peux utiliser Task.ContinueWith pour attendre de manière asynchrone que la tâche se termine (c'est-à-dire planifier une action à exécuter lorsque la tâche est terminée), mais cela ne permet pas de spécifier un délai d'expiration. Je peux utiliser Task.Wait pour attendre de manière synchrone que la tâche se termine avec un délai d'attente, 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?

dtb
la source
3
Tu as raison. Je suis surpris qu'il ne prévoie pas de délai d'attente. Peut-être dans .NET 5.0 ... Bien sûr, nous pouvons intégrer le délai d'attente dans la tâche elle-même, mais ce n'est pas bon, ces choses doivent être gratuites.
Aliostad
4
Bien qu'il nécessiterait toujours une logique pour le délai d'expiration à deux niveaux que vous décrivez, .NET 4.5 propose en effet une méthode simple pour créer un délai d'expiration CancellationTokenSource. Deux surcharges pour le constructeur sont disponibles, l'une prenant un délai milliseconde entier et l'autre prenant un retard TimeSpan.
patridge
La source complète de la
une solution finale avec le code source complet qui fonctionne? exemple peut-être plus complexe pour signaler les erreurs dans chaque thread et après WaitAll affiche un résumé?
Kiquenet

Réponses:

566

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 MS Parallel Library) avec plus d'informations sur ce genre de choses .

Ajout : à la demande d'un commentaire sur ma réponse, voici une solution élargie qui inclut la gestion des annulations. Notez que le fait de passer l'annulation à la tâche et au minuteur signifie qu'il existe plusieurs façons d'annuler votre code, et vous devez vous assurer de tester et d'être sûr de les gérer correctement. Ne laissez pas au hasard diverses combinaisons et espérons que votre ordinateur fera ce qu'il faut lors 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
}
Andrew Arnott
la source
86
Il convient de mentionner que même si Task.Delay peut se terminer avant une longue tâche, ce qui vous permet de gérer un scénario d'expiration, cela n'annule PAS la longue tâche elle-même; WhenAny vous fait simplement savoir que l'une des tâches qui lui sont transmises est terminée. Vous devrez implémenter un CancellationToken et annuler vous-même la tâche de longue durée.
Jeff Schumacher
30
Il peut également être noté que la Task.Delaytâche est soutenue par un temporisateur système qui continuera à être suivi jusqu'à l'expiration du délai d'expiration, quelle que soit la durée SomeOperationAsync. Donc, si cet extrait de code global s'exécute beaucoup dans une boucle serrée, vous consommez des ressources système pour les temporisateurs jusqu'à ce qu'ils expirent tous. La façon de résoudre ce problème serait d'avoir un CancellationTokenfichier que vous transmettez à Task.Delay(timeout, cancellationToken)celui que vous annulez à la SomeOperationAsyncfin pour libérer la ressource du minuteur.
Andrew Arnott
12
Le code d'annulation fait trop de travail. Essayez ceci: int timeout = 1000; var cancelTokenSource = new CancellationTokenSource (timeout); var cancelToken = tokenSource.Token; var task = SomeOperationAsync (cancelToken); essayez {attendre la tâche; // Ajoutez ici du code pour la réussite} catch (OperationCancelledException) {// Ajoutez ici du code pour le délai d'expiration}
srm
3
@ilans en attendant le Task, toute exception stockée par la tâche est renvoyée à ce stade. Cela vous donne une chance de rattraper OperationCanceledException(en cas d'annulation) ou toute autre exception (en cas de faute).
Andrew Arnott
3
@TomexOu: la question était de savoir comment attendre de manière asynchrone la fin d'une tâche. Task.Wait(timeout)bloquerait de manière synchrone au lieu d'attendre de manière asynchrone.
Andrew Arnott
221

Voici une version de méthode d'extension qui incorpore l'annulation du délai d'expiration lorsque la tâche d'origine se termine, 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.");
        }
    }
}
Lawrence Johnston
la source
8
Donnez à cet homme des voix. Solution élégante. Et si votre appel n'a pas de type de retour, assurez-vous de supprimer le TResult.
Lucas
6
CancellationTokenSource est jetable et devrait être dans un usingbloc
PeterM
6
@ It'satrap Attendre une tâche deux fois renvoie simplement le résultat à la seconde attente. Il ne s'exécute pas deux fois. On pourrait dire qu'il est égal task.Result lorsqu'il est exécuté deux fois.
M. Mimpen
7
La tâche d'origine ( task) continuera-t-elle toujours à s'exécuter en cas d'expiration?
jag
6
Possibilité d'amélioration mineure: TimeoutExceptiona un message par défaut approprié. Le remplacer par "L'opération a expiré." n'ajoute aucune valeur et provoque en fait une certaine confusion en impliquant qu'il y a une raison de la remplacer.
Edward Brey
49

Vous pouvez utiliser Task.WaitAnypour 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 d'expiration spécifiés), puis utiliser WaitAnypour attendre la première des éventualités. Si la tâche qui s'est terminée en premier est votre tâche "de travail", alors vous avez terminé. Si la tâche qui s'est terminée en premier est une tâche d'expiration, vous pouvez alors réagir à l'expiration (par exemple, demander l'annulation).

Tomas Petricek
la source
1
J'ai vu cette technique utilisée par un MVP que je respecte vraiment, elle me semble beaucoup plus propre que la réponse acceptée. Peut-être qu'un exemple aiderait à obtenir plus de votes! Je serais volontaire pour le faire, sauf que je n'ai pas suffisamment d'expérience pour être sûr que ce serait utile :)
GrahamMc
3
un thread serait bloqué - mais si vous êtes d'accord avec cela, alors aucun problème. La solution que j'ai prise était celle ci-dessous, car aucun thread n'est bloqué. J'ai lu le billet de blog qui était vraiment bien.
JJschk
@JJschk vous mentionnez que vous avez pris la solution below.... c'est quoi? sur la base d'une commande SO?
BozoJoe
et si je ne veux pas que la tâche plus lente soit annulée? Je veux le gérer quand il a fini mais reviens de la méthode actuelle ..
Akmal Salikhov
18

Et 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.

as-cii
la source
En fait, dans cet exemple, vous n'attendez pas à l'intérieur de t1 mais sur une tâche supérieure. Je vais essayer de faire un exemple plus détaillé.
as-cii
14

Voici un exemple entièrement travaillé basé sur la réponse la plus votée, 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 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 enveloppée dans une fonction de temporisation, 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();
            }
        }
    }
}

Avertissements

Après avoir donné cette réponse, ce n'est généralement pas une bonne pratique d'avoir des exceptions levées dans votre code pendant le fonctionnement normal, 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 étroite.

N'utilisez ce code que si vous ne pouvez absolument pas modifier la fonction que vous appelez, de sorte qu'elle expire après un moment précis TimeSpan.

Cette réponse n'est vraiment applicable que lorsqu'il s'agit de 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:

Chaque opération qui pourrait potentiellement se bloquer indéfiniment doit avoir un délai d'expiration.

Si vous n'observez pas cette règle, votre code finira par frapper une opération qui échoue pour une raison quelconque, puis il se bloquera indéfiniment et votre application vient de se bloquer définitivement.

S'il y avait un délai raisonnable après un certain temps, votre application se bloquerait pendant une durée extrême (par exemple 30 secondes), puis elle afficherait une erreur et continuerait son chemin joyeux, ou réessayer.

Contango
la source
11

En utilisant l'excellente bibliothèque AsyncEx de Stephen Cleary , vous pouvez faire:

TimeSpan timeout = TimeSpan.FromSeconds(10);

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

TaskCanceledException sera lancé en cas de timeout.

Cocowalla
la source
10

Il s'agit d'une version légèrement améliorée des réponses précédentes.

  • En plus de la réponse de Lawrence , il annule la tâche d'origine lorsque le délai d'expiration se produit.
  • En plus des variantes de réponse 2 et 3 de sjb , vous pouvez prévoir CancellationTokenla tâche d'origine, et lorsque le délai d'expiration se produit, vous obtenez à la TimeoutExceptionplace 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, timeoutCancellation.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();
        }
    }
}

Usage

InnerCallAsyncpeut prendre beaucoup de temps. CallAsyncl'enveloppe avec un délai d'attente.

async Task<int> CallAsync(CancellationToken cancellationToken)
{
    var timeout = TimeSpan.FromMinutes(1);
    int result = await CancelAfterAsync(ct => InnerCallAsync(ct), timeout,
        cancellationToken);
    return result;
}

async Task<int> InnerCallAsync(CancellationToken cancellationToken)
{
    return 42;
}
Josef Bláha
la source
1
Merci pour la solution! On dirait que vous devriez passer timeoutCancellationdans delayTask. Actuellement, si vous déclenchez l'annulation, CancelAfterAsyncvous pouvez lancer TimeoutExceptionau lieu de TaskCanceledException, car la cause delayTaskpeut être terminée en premier.
AxelUser
@AxelUser, vous avez raison. Il m'a fallu une heure avec un tas de tests unitaires pour comprendre ce qui se passait :) J'ai supposé que lorsque les deux tâches données à WhenAnysont annulées par le même jeton, WhenAnyretournera la première tâche. Cette hypothèse était fausse. J'ai édité la réponse. Merci!
Josef Bláha
J'ai du mal à trouver comment réellement appeler cela avec une fonction Task <SomeResult> définie; Y a-t-il une chance que vous puissiez saisir un exemple de comment l'appeler?
jhaagsma
1
@jhaagsma, exemple ajouté!
Josef Bláha
@ JosefBláha Merci beaucoup! J'enroule lentement ma tête autour de la syntaxe de style lambda, cela ne me serait pas venu à l'esprit - que le jeton est passé à la tâche dans le corps de CancelAfterAsync, en passant la fonction lambda. Nifty!
jhaagsma
8

Utilisez une minuterie pour gérer le message et l'annulation automatique. Une fois la tâche terminée, appelez Dispose sur les minuteries afin qu'elles ne se déclenchent jamais. Voici un exemple; changez 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 CTP Async fournit une méthode TaskEx.Delay qui encapsulera les temporisateurs dans les tâches pour vous. Cela peut vous donner plus de contrôle pour faire des choses comme définir le TaskScheduler pour la continuation lorsque le Timer se déclenche.

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;
}
Quartermeister
la source
Il ne veut pas que le thread actuel soit bloqué, c'est-à-dire non task.Wait().
Cheng Chen
@Danny: C'était juste pour compléter l'exemple. Après le ContinueWith, vous pouvez revenir et laisser la tâche s'exécuter. Je mettrai à jour ma réponse pour que ce soit plus clair.
Quartermeister
2
@dtb: que faire si vous faites de t1 une tâche <tâche <résultat>> puis appelez TaskExtensions.Unwrap? Vous pouvez renvoyer t2 à partir de votre lambda intérieur et vous pouvez ensuite ajouter des continuations à la tâche non encapsulée.
Quartermeister
Impressionnant! Cela résout parfaitement mon problème. Merci! Je pense que j'irai avec la solution proposée par @ AS-CII, bien que j'aimerais pouvoir également accepter votre réponse pour avoir suggéré TaskExtensions.Unwrap Dois-je ouvrir une nouvelle question afin que vous puissiez obtenir le représentant que vous méritez?
dtb
6

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 pourriez avoir 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;
Kevan
la source
4

Une version générique de la réponse de @ Kevan ci-dessus, utilisant des 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 Scheduler en option:

public static Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout, Scheduler scheduler = null)
{
    return scheduler is null 
       ? task.ToObservable().Timeout(timeout).ToTask() 
       : task.ToObservable().Timeout(timeout, scheduler).ToTask();
}

BTW: Lorsqu'un Timeout se produit, une exception de timeout sera levée

Jasper H Bojsen
la source
0

Si vous utilisez un 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 a un jeton d'expiration et d'annulation intégré.

kns98
la source
Je devrais écrire quelque chose (je ne veux pas mettre de code propriétaire ici) mais le scénario est comme ça. Le producteur sera le code qui exécute la méthode qui pourrait expirer et mettra les résultats dans la file d'attente une fois terminé. Le consommateur appellera trytake () avec délai d'expiration et recevra le jeton à l'expiration du délai. Le producteur et le consommateur seront des tâches inversées et afficheront un message à l'utilisateur en utilisant le répartiteur de threads de l'interface utilisateur si nécessaire.
kns98
0

J'ai ressenti la Task.Delay()tâche et CancellationTokenSourcedans les autres réponses un peu pour mon cas d'utilisation dans une boucle de réseau serrée.

Et bien que la méthode Crafting a Task.TimeoutAfter de Joe Hoag sur les blogs MSDN ait été inspirante, j'étais un peu fatigué d'utiliser TimeoutExceptionpour le contrôle de flux pour la même raison que ci-dessus, car des délais d'attente sont attendus plus souvent qu'autrement.

Je suis donc 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;
    }
}

Un exemple de cas 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;
antak
la source
0

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

  1. Si vous souhaitez attendre une tâche existante et savoir si elle s'est terminée ou a expiré, mais ne souhaitez pas l'annuler si le délai d'expiration 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 souhaitez démarrer une tâche de travail et annuler le travail si le délai d'expiration 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 déjà créé une tâche que vous souhaitez annuler en cas d'expiration:

    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 temporisateur si le délai d'expiration ne se produit pas, donc plusieurs appels n'entraîneront pas le cumul des temporisateurs.

sjb

sjb-sjb
la source
0

Je recompose les idées d'autres réponses ici et cette réponse sur un autre fil dans une méthode d'extension de style Try. Cela présente un avantage si vous souhaitez une méthode d'extension, tout en évitant une exception lors de la temporisation.

public static async Task<bool> TryWithTimeoutAfter<TResult>(this Task<TResult> task,
    TimeSpan timeout, Action<TResult> successor)
{

    using var timeoutCancellationTokenSource = new CancellationTokenSource();
    var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token))
                                  .ConfigureAwait(continueOnCapturedContext: false);

    if (completedTask == task)
    {
        timeoutCancellationTokenSource.Cancel();

        // propagate exception rather than AggregateException, if calling task.Result.
        var result = await task.ConfigureAwait(continueOnCapturedContext: false);
        successor(result);
        return true;
    }
    else return false;        
}     

async Task Example(Task<string> task)
{
    string result = null;
    if (await task.TryWithTimeoutAfter(TimeSpan.FromSeconds(1), r => result = r))
    {
        Console.WriteLine(result);
    }
}    
tm1
la source
-3

Certainement pas, mais c'est une option si ... Je ne peux pas penser à une raison valable.

((CancellationTokenSource)cancellationToken.GetType().GetField("m_source",
    System.Reflection.BindingFlags.NonPublic |
    System.Reflection.BindingFlags.Instance
).GetValue(cancellationToken)).Cancel();
syko9000
la source