Aguarde de forma assíncrona que a Tarefa <T> seja concluída com tempo limite

388

Quero aguardar que uma tarefa <T> seja concluída com algumas regras especiais: se ela não for concluída após X milissegundos, desejo exibir uma mensagem para o usuário. E se não for concluído após milissegundos Y, desejo solicitar automaticamente o cancelamento .

Eu posso usar Task.ContinueWith para aguardar assincronamente a tarefa (ou seja, agendar uma ação a ser executada quando a tarefa for concluída), mas isso não permite especificar um tempo limite. Posso usar o Task.Wait para aguardar de forma síncrona que a tarefa seja concluída com um tempo limite, mas isso bloqueia meu encadeamento. Como posso aguardar assincronamente a conclusão da tarefa com um tempo limite?

dtb
fonte
3
Você está certo. Estou surpreso que ele não preveja tempo limite. Talvez no .NET 5.0 ... É claro que podemos criar o tempo limite para a tarefa em si, mas isso não é bom, essas coisas devem ser liberadas.
Aliostad
4
Embora ainda exija lógica para o tempo limite de duas camadas que você descreve, o .NET 4.5 realmente oferece um método simples para criar um tempo limite baseado em tempo limite CancellationTokenSource. Estão disponíveis duas sobrecargas para o construtor, uma tendo um atraso de milissegundos inteiro e outra com um atraso de TimeSpan.
patridge
A fonte da biblioteca simples e completa aqui: stackoverflow.com/questions/11831844/…
alguma solução final com código fonte completo funcionando? exemplo talvez mais complexo para notificar erros em cada thread e depois de WaitAll mostrar um resumo?
Kiquenet

Respostas:

566

Que tal agora:

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

E aqui está uma excelente postagem no blog "Criando um método Task.TimeoutAfter" (da equipe da MS Parallel Library) com mais informações sobre esse tipo de coisa .

Além disso : a pedido de um comentário na minha resposta, aqui está uma solução expandida que inclui tratamento de cancelamento. Observe que passar o cancelamento para a tarefa e o cronômetro significa que há várias maneiras de o cancelamento ocorrer no seu código, e você deve testar e ter certeza de que lida com todos eles corretamente. Não deixe ao acaso várias combinações e espere que seu computador faça a coisa certa em tempo de execução.

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
fonte
86
Deve-se mencionar que, embora Task.Delay possa ser concluído antes da tarefa de longa execução, permitindo que você lide com um cenário de tempo limite, isso NÃO cancela a própria tarefa de longa execução; WhenAny simplesmente informa que uma das tarefas transmitidas foi concluída. Você terá que implementar um CancellationToken e cancelar a tarefa de longa duração.
Jeff Schumacher
30
Também é possível observar que a Task.Delaytarefa é apoiada por um cronômetro do sistema que continuará sendo rastreado até o tempo limite expirar, independentemente de quanto tempo SomeOperationAsyncleva. Portanto, se esse snippet de código geral executar muito em um loop apertado, você estará consumindo recursos do sistema para temporizadores até o tempo limite. A maneira de corrigir isso seria ter uma CancellationTokenpassagem Task.Delay(timeout, cancellationToken)que você cancela ao SomeOperationAsyncconcluir para liberar o recurso de timer.
Andrew Arnott
12
O código de cancelamento está fazendo muito trabalho. Tente o seguinte: int timeout = 1000; var cancelamentoTokenSource = novo CancellationTokenSource (timeout); var cancelamentoToken = tokenSource.Token; var tarefa = SomeOperationAsync (cancelellationToken); tente {aguardar tarefa; // Adicione aqui o código para a conclusão bem-sucedida} catch (OperationCancelledException) {// Adicione o código aqui para o caso de tempo limite}
srm
3
@ilans, aguardando Task, qualquer exceção armazenada pela tarefa é mostrada novamente nesse ponto. Isso permite que você pegue OperationCanceledException(se cancelado) ou qualquer outra exceção (se houver falha).
Andrew Arnott
3
@ TomxOu: a questão era como aguardar assincronamente a conclusão de uma tarefa. Task.Wait(timeout)bloquearia síncrona em vez de aguardar assincronamente.
Andrew Arnott 03/02
221

Aqui está uma versão do método de extensão que incorpora o cancelamento do tempo limite quando a tarefa original é concluída, conforme sugerido por Andrew Arnott em um comentário à sua resposta .

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
fonte
8
Dê a este homem alguns votos. Solução elegante. E se sua chamada não tiver um tipo de retorno, remova o TResult.
Lucas
6
CancellationTokenSource é descartável e deve estar em um usingbloco
peterm
6
@ It'satrap Aguardar uma tarefa duas vezes simplesmente retorna o resultado na segunda espera. Não executa duas vezes. Você poderia dizer que é igual task.Result quando executado duas vezes.
26417 M. Mimpen
7
A tarefa original ( task) ainda continuará em execução no caso de um tempo limite?
jag
6
Oportunidade para melhorias menores: TimeoutExceptionpossui uma mensagem padrão adequada. Substituindo-o por "A operação atingiu o tempo limite". não agrega valor e, na verdade, causa alguma confusão, implicando que há uma razão para substituí-lo.
Edward Brey
49

Você pode usar Task.WaitAnypara aguardar a primeira de várias tarefas.

Você pode criar duas tarefas adicionais (que são concluídas após os tempos limite especificados) e depois usar WaitAnypara aguardar o que for concluído primeiro. Se a tarefa que foi concluída primeiro for a sua "obra", você estará pronto. Se a tarefa que foi concluída primeiro for uma tarefa de tempo limite, você poderá reagir ao tempo limite (por exemplo, solicitar cancelamento).

Tomas Petricek
fonte
11
Eu já vi essa técnica usada por um MVP que eu realmente respeito, me parece muito mais limpo do que a resposta aceita. Talvez um exemplo ajude a obter mais votos! Eu voluntariar para fazer isso, exceto que eu não tenho bastante experiência de tarefas para estar confiante de que seria útil :)
GrahamMc
3
um segmento seria bloqueado - mas se você estiver de acordo com isso, não há problema. A solução que tomei foi a abaixo, pois nenhum segmento está bloqueado. Eu li o post do blog que foi muito bom.
21413 JJschk
@JJschk você mencionou que pegou a solução below.... qual é? com base no pedido de SO?
BozoJoe
e se eu não quiser que uma tarefa mais lenta seja cancelada? Eu quero lidar com isso quando terminar, mas retornar do método atual ..
Akmal Salikhov 24/01
18

Que tal algo assim?

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

Você pode usar a opção Task.Wait sem bloquear o thread principal usando outra tarefa.

as-cii
fonte
De fato, neste exemplo, você não está esperando dentro de t1, mas em uma tarefa superior. Vou tentar fazer um exemplo mais detalhado.
que você precisa saber é o seguinte
14

Aqui está um exemplo completo, com base na resposta mais votada, que é:

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

A principal vantagem da implementação nesta resposta é que os genéricos foram adicionados, para que a função (ou tarefa) possa retornar um valor. Isso significa que qualquer função existente pode ser agrupada em uma função de tempo limite, por exemplo:

Antes:

int x = MyFunc();

Depois de:

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

Este código requer o .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();
            }
        }
    }
}

Ressalvas

Tendo dado essa resposta, geralmente não é uma boa prática ter exceções lançadas em seu código durante a operação normal, a menos que você precise:

  • Cada vez que uma exceção é lançada, é uma operação extremamente pesada,
  • Exceções podem tornar seu código lento por um fator de 100 ou mais se as exceções estiverem em um loop restrito.

Somente use esse código se você absolutamente não puder alterar a função que está chamando, para que o tempo limite seja excedido após um determinado TimeSpan.

Essa resposta é realmente aplicável apenas ao lidar com bibliotecas de bibliotecas de terceiros que você simplesmente não pode refatorar para incluir um parâmetro de tempo limite.

Como escrever código robusto

Se você deseja escrever um código robusto, a regra geral é esta:

Toda operação que pode potencialmente bloquear indefinidamente deve ter um tempo limite.

Se você não observar essa regra, seu código acabará atingindo uma operação que falhará por algum motivo, bloqueará indefinidamente e seu aplicativo será interrompido permanentemente.

Se houvesse um tempo limite razoável após algum tempo, seu aplicativo seria interrompido por um período extremo de tempo (por exemplo, 30 segundos). Ele exibia um erro e continuava em seu caminho alegre ou tentava novamente.

Contango
fonte
11

Usando a excelente biblioteca AsyncEx de Stephen Cleary , você pode:

TimeSpan timeout = TimeSpan.FromSeconds(10);

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

TaskCanceledException será lançado no caso de um tempo limite.

Cocowalla
fonte
10

Esta é uma versão ligeiramente aprimorada das respostas anteriores.

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

Uso

InnerCallAsyncpode levar muito tempo para concluir. CallAsyncenvolve com um tempo limite.

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
fonte
11
Obrigado pela solução! Parece que você deve passar timeoutCancellationpara delayTask. Atualmente, se você acionar o cancelamento, CancelAfterAsyncpode lançar em TimeoutExceptionvez de TaskCanceledException, a causa delayTaskpode terminar primeiro.
AxelUser 23/10/19
@AxelUser, você está certo. Levei uma hora com vários testes de unidade para entender o que estava acontecendo :) Presumi que, quando as duas tarefas atribuídas WhenAnyforem canceladas pelo mesmo token, WhenAnyretornará a primeira tarefa. Essa suposição estava errada. Eu editei a resposta. Obrigado!
Josef Bláha 24/10/19
Estou tendo dificuldade para descobrir como realmente chamar isso com uma função definida Task <SomeResult>; alguma chance de você seguir um exemplo de como chamá-lo?
jhaagsma
11
@jhaagsma, exemplo adicionado!
Josef Bláha
@ JosefBláha Muito obrigado! Ainda estou lentamente envolvendo minha sintaxe no estilo lambda, o que não teria ocorrido comigo - que o token é passado para a tarefa no corpo do CancelAfterAsync, passando a função lambda. Bacana!
jhaagsma
8

Use um timer para lidar com a mensagem e o cancelamento automático. Quando a tarefa terminar, chame Dispose nos timers para que eles nunca disparem. Aqui está um exemplo; altere taskDelay para 500, 1500 ou 2500 para ver os diferentes casos:

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

Além disso, o CTP assíncrono fornece um método TaskEx.Delay que agrupará os cronômetros em tarefas para você. Isso pode lhe dar mais controle para executar ações como definir o TaskScheduler para a continuação quando o Timer for disparado.

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
fonte
Ele não quer que o segmento atual seja bloqueado, ou seja, não task.Wait().
Cheng Chen
@ Danny: Isso foi apenas para tornar o exemplo completo. Após o ContinueWith, você poderia retornar e deixar a tarefa executar. Vou atualizar minha resposta para deixar isso mais claro.
Quartermeister
2
@ dtb: E se você tornar t1 uma tarefa <tarefa <resultado>> e depois chamar TaskExtensions.Unwrap? Você pode retornar t2 do seu lambda interno e adicionar continuações à tarefa desembrulhada posteriormente.
Quartermeister
Impressionante! Isso resolve perfeitamente meu problema. Obrigado! Acho que vou com a solução proposta pelo @ AS-CII, embora deseje poder aceitar sua resposta também por sugerir TaskExtensions.Unwrap Devo abrir uma nova pergunta para que você possa obter o representante que merece?
DTB
6

Outra maneira de resolver esse problema é usar extensões reativas:

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

Teste acima usando o código abaixo no seu teste de unidade, ele funciona para mim

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

Você pode precisar do seguinte espaço para nome:

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
fonte
4

Uma versão genérica da resposta de @ Kevan acima, usando Extensões Reativas.

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

Com o Agendador opcional:

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: Quando ocorrer um tempo limite, uma exceção de tempo limite será lançada

Jasper H Bojsen
fonte
0

Se você usar um BlockingCollection para agendar a tarefa, o produtor poderá executar a tarefa potencialmente demorada e o consumidor poderá usar o método TryTake, que possui um token de tempo limite e cancelamento incorporado.

kns98
fonte
Eu teria que escrever algo (não quero colocar código proprietário aqui), mas o cenário é assim. O produtor será o código que executa o método que pode atingir o tempo limite e colocará os resultados na fila quando terminar. O consumidor chamará trytake () com tempo limite e receberá o token após o tempo limite. O produtor e o consumidor farão tarefas de backround e exibirão uma mensagem para o usuário usando o despachante de threads da interface do usuário, se necessário.
kns98
0

Eu senti a Task.Delay()tarefa e CancellationTokenSourcenas outras respostas um pouco demais para o meu caso de uso em um loop restrito de rede.

E embora o método Crafting a Task.TimeoutAfter de Joe Hoag nos blogs do MSDN tenha sido inspirador, eu estava um pouco cansado de usar o TimeoutExceptioncontrole de fluxo pelo mesmo motivo acima, porque os tempos limite são esperados com mais frequência do que não.

Então eu fui com isso, que também lida com as otimizações mencionadas no 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;
    }
}

Um exemplo de caso de uso é o seguinte:

var receivingTask = conn.ReceiveAsync(ct);

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

// Read and do something with data
var data = await receivingTask;
Antak
fonte
0

Algumas variantes da resposta de Andrew Arnott:

  1. Se você deseja aguardar uma tarefa existente e descobrir se ela foi concluída ou excedeu o tempo limite, mas não deseja cancelá-la se ocorrer o tempo limite:

    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. Se você deseja iniciar uma tarefa de trabalho e cancelar o trabalho se ocorrer o tempo limite:

    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. Se você já tiver uma tarefa criada que deseja cancelar se ocorrer um tempo limite:

    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
    }

Outro comentário: essas versões cancelarão o cronômetro se o tempo limite não ocorrer, portanto, várias chamadas não farão com que os cronômetros se acumulem.

sjb

sjb-sjb
fonte
0

Estou recombuindo as idéias de algumas outras respostas aqui e essa resposta em outro thread em um método de extensão no estilo Try. Isso tem um benefício se você deseja um método de extensão, evitando uma exceção após o tempo limite.

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
fonte
-3

Definitivamente não faça isso, mas é uma opção se ... Não consigo pensar em um motivo válido.

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