Асинхронно ждать Task <T>, чтобы завершить таймаут

Я хочу подождать Задача <T> , чтобы выполнить некоторые специальные правила: Если он не завершился после X миллисекунд, я хочу отобразить сообщение пользователю. И если он не завершился после Y миллисекунд, я хочу автоматически отменить отмену.

Я могу использовать Task.ContinueWith для асинхронного ожидания завершения задачи (т.е. планировать действие, которое будет выполняться при завершении задачи), но что не позволяет указать тайм-аут. Я могу использовать Task.Wait для синхронного ожидания завершения задачи с таймаутом, но это блокирует мой поток. Как я могу асинхронно ждать завершения задачи с тайм-аутом?

Ответ 1

Как насчет этого:

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

И здесь отличная запись в блоге "Создание задачи. TimeoutAfter Method" (из команды MS Parallel Library) с дополнительной информацией об этом виде.

Дополнение: по запросу комментария к моему ответу, это расширенное решение, которое включает обработку отмены. Обратите внимание, что передача отмены задания и таймера означает, что в вашем коде может быть обнаружено несколько способов отмены, и вы должны быть уверены, что будете проверять и быть уверенными, что правильно их обрабатываете. Не оставляйте на случайных комбинаций и надейтесь, что ваш компьютер сделает все правильно во время выполнения.

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
}

Ответ 2

Здесь версия метода расширения, включающая отмену таймаута, когда исходная задача завершена, как предложил Эндрю Арнотт в комментарии к его ответ.

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

Ответ 3

Вы можете использовать Task.WaitAny для ожидания первой из нескольких задач.

Вы можете создать две дополнительные задачи (завершающиеся после указанных тайм-аутов), а затем использовать WaitAny, чтобы дождаться того, что наступит раньше. Если задача, которая была выполнена в первый раз, является вашей "рабочей" задачей, тогда все готово. Если задача, которая была выполнена первой, - это задача тайм-аута, вы можете отреагировать на таймаут (например, аннулирование запроса).

Ответ 4

Как насчет этого?

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

Вы можете использовать параметр Task.Wait, не блокируя основной поток, используя другую задачу.

Ответ 5

Вот полностью обработанный пример, основанный на верхнем голосовавшем ответе, который:

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

Основным преимуществом реализации в этом ответе является добавление дженериков, поэтому функция (или задача) может вернуть значение. Это означает, что любая существующая функция может быть завернута в функцию тайм-аута, например:

До:

int x = MyFunc();

После:

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

Для этого кода требуется .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();
            }
        }
    }
}

Предостережение

Получив этот ответ, обычно не рекомендуется применять исключения в вашем коде во время нормальной работы, если только вам не нужно:

  • Каждый раз, когда генерируется исключение, его чрезвычайно тяжелая операция,
  • Исключения могут замедлить ваш код до 100 или более раз, если исключения находятся в плотном цикле.

Используйте этот код только в том случае, если вы абсолютно не можете изменить функцию, которую вы вызываете, поэтому она истекает после определенного TimeSpan.

Этот ответ действительно применим только при работе с библиотеками сторонних библиотек, которые вы просто не можете реорганизовать, чтобы включить параметр таймаута.

Как написать надежный код

Если вы хотите написать надежный код, общим правилом является следующее:

Каждая операция, которая может потенциально блокировать бесконечно, должна иметь тайм-аут.

Если вы не соблюдаете это правило, ваш код в конечном итоге ударит по какой-либо операции, которая по какой-то причине не работает, тогда она будет блокироваться бесконечно, и ваше приложение просто постоянно висит.

Если после некоторого времени был разумный тайм-аут, ваше приложение зависало бы в течение некоторого времени (например, 30 секунд), тогда оно либо вывело бы ошибку, либо продолжило бы свой веселый путь, либо повторит попытку.

Ответ 6

Используйте Timer для обработки сообщения и автоматической отмены. Когда задача завершена, вызовите Dispose на таймеры, чтобы они никогда не срабатывали. Вот пример; change taskDelay до 500, 1500 или 2500 для просмотра различных случаев:

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

Кроме того, Async CTP предоставляет метод TaskEx.Delay, который будет включать таймеры в задачи для вас. Это может дать вам больше возможностей для выполнения таких действий, как установка TaskScheduler для продолжения при срабатывании таймера.

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

Ответ 7

Используя превосходную библиотеку Стивена Клири AsyncEx, вы можете сделать:

TimeSpan timeout = TimeSpan.FromSeconds(10);

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

TaskCanceledException будет выброшено в случае тайм-аута.

Ответ 8

Другим способом решения этой проблемы является использование Reactive Extensions:

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

Проверьте выше, используя приведенный ниже код в unit test, он работает для меня

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

Возможно, вам понадобится следующее пространство имен:

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;

Ответ 9

Общая версия ответа @Kevan выше с реактивными расширениями.

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

С дополнительным планировщиком:

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: Когда произойдет Тайм-аут, будет выведено исключение тайм-аута

Ответ 10

Если вы планируете выполнять задачу BlockingCollection, производитель может запустить потенциально долго выполняющуюся задачу, и потребитель может использовать метод TryTake, в котором встроен маркер времени ожидания и отмены.

Ответ 11

Это слегка улучшенная версия предыдущих ответов.

  • В дополнение к ответу Лоуренса он отменяет исходное задание по истечении времени ожидания.
  • В дополнение к вариантам ответов sjb 2 и 3, вы можете предоставить CancellationToken для исходной задачи, а по истечении времени ожидания вы получите TimeoutException вместо 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();
        }
    }
}

Ответ 12

Несколько вариантов ответа Эндрю Арнотта:

  1. Если вы хотите подождать существующее задание и выяснить, завершено ли оно или истекло время ожидания, но не хотите отменить его, если истечет время ожидания:

    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. Если вы хотите запустить рабочую задачу и отменить ее, если истекло время ожидания:

    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. Если у вас уже есть задача, которую вы хотите отменить в случае истечения времени ожидания:

    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
    }
    

Еще один комментарий, эти версии будут отменять таймер, если тайм-аут не происходит, поэтому несколько вызовов не приведет к накоплению таймеров.

SJB

Ответ 13

Я почувствовал, что задача Task.Delay() и CancellationTokenSource в других немного отвечают за мой случай использования в тесном сетевом цикле.

И хотя Джо Хоаг "Создание метода Task.TimeoutAfter" в блогах MSDN был вдохновляющим, я немного устал от использования TimeoutException для управления потоком по той же причине, что и выше, потому что время ожидания ожидается чаще, чем нет..

Итак, я пошел с этим, который также обрабатывает оптимизации, упомянутые в блоге:

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

Пример использования:

var receivingTask = conn.ReceiveAsync(ct);

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

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