Pergunta

Eu quero esperar por um Tarefau003CT> Para concluir com algumas regras especiais: se não tiver concluído após x milissegundos, quero exibir uma mensagem para o usuário. E se não tiver sido concluído depois de milissegundos, eu quero automaticamente Requisitar cancelamento.

Eu posso usar Tarefa.ContinueWith Aguarde de forma assíncrona para que a tarefa seja concluída (ou seja, agende uma ação a ser executada quando a tarefa estiver concluída), mas isso não permite especificar um tempo limite. Eu posso usar Task.wait Para aguardar síncrono, a tarefa concluir com um tempo limite, mas isso bloqueia meu thread. Como posso esperar de maneira assíncrona a conclusão da tarefa com um tempo limite?

Foi útil?

Solução

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 ótima postagem no blog "criando uma tarefa..

Adição: A pedido de um comentário sobre minha resposta, aqui está uma solução expandida que inclui o manuseio de cancelamento. Observe que a passagem do cancelamento para a tarefa e o timer significa que existem várias maneiras pelas quais o cancelamento pode ser experimentado em seu código, e você deve testar e ter certeza de que você lida adequadamente com todos eles. Não saia ao acaso várias combinações e espero 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
}

Outras dicas

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

Você pode usar Task.WaitAny para esperar a primeira de várias tarefas.

Você pode criar duas tarefas adicionais (que completas após os tempos limitados especificados) e depois usar WaitAny para esperar por qualquer concluir primeiro. Se a tarefa concluída primeiro é a sua tarefa de "trabalho", terminou. Se a tarefa concluída primeiro for uma tarefa de tempo limite, você poderá reagir ao tempo limite (por exemplo, cancelamento da solicitação).

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.

Aqui está um exemplo totalmente trabalhado com base na resposta votada superior, 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; portanto, a função (ou tarefa) pode retornar um valor. Isso significa que qualquer função existente pode ser envolvida em uma função de tempo limite, por exemplo:

Antes da:

int x = MyFunc();

Depois:

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

Este código requer .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 esta resposta, é geralmente não Uma boa prática para ter exceções jogadas em seu código durante a operação normal, a menos que você absolutamente precise:

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

Use apenas este código se você absolutamente não puder alterar a função que está ligando para que ele se sinta depois de um específico TimeSpan.

Essa resposta é realmente aplicável ao lidar com as 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 código robusto, a regra geral é a seguinte:

Todas as operações que poderiam bloquear indefinidamente, devem ter um tempo limite.

Se você não faça Observe essa regra, seu código acabará por atingir uma operação que falha por algum motivo, então ela bloqueará indefinidamente e seu aplicativo acaba de pendurar permanentemente.

Se houvesse um tempo limite razoável depois de algum tempo, seu aplicativo ficaria por algum tempo extremo de tempo (por exemplo, 30 segundos), ele exibiria um erro e continuará em seu caminho alegre ou novamente.

Use um Cronômetro Para lidar com a mensagem e o cancelamento automático. Quando a tarefa for concluída, ligue para descartar os temporizadores para que eles nunca disparem. Aqui está um exemplo; Altere o 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);
                }
            }
        }
    }
}

Também o ASYNC CTP Fornece um método Taskex.Delay que envolverá os temporizadores nas tarefas para você. Isso pode fornecer mais controle para fazer coisas como definir o TaskScheduler para a continuação quando o timer disparar.

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

Usando o excelente de Stephen Cleary Assíncex Biblioteca, você pode fazer:

TimeSpan timeout = TimeSpan.FromSeconds(10);

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

TaskCanceledException será jogado no caso de um tempo limite.

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

Uma versão genérica da resposta de @Kevan acima com 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 agendador opcional:

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

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

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

  • Além de Resposta de Lawrence, cancela a tarefa original quando o tempo limite ocorre.
  • Adicional a Variantes de resposta do SJB 2 e 3, você pode fornecer CancellationToken Para a tarefa original e quando ocorre o tempo limite, você fica TimeoutException ao invés 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();
        }
    }
}

Algumas variantes da resposta de Andrew Arnott:

  1. Se você quiser esperar por uma tarefa existente e descobrir se ela concluiu ou cronometrada, mas não quiser cancelá -la se o tempo limite ocorrer:

    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ê quiser 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á possui 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 temporizadores se acumulem.

SJB

Eu senti o Task.Delay() tarefa e CancellationTokenSource Nas outras respostas, muito para o meu caso de uso em um loop de rede apertado.

E embora Joe Hoag está criando uma tarefa. foi inspirador, eu estava um pouco cansado de usar TimeoutException Para o controle de fluxo pelo mesmo motivo que acima, porque os tempos limites 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 é como tal:

var receivingTask = conn.ReceiveAsync(ct);

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

// Read and do something with data
var data = await receivingTask;
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top