Асинхронно ждать завершения задачи с таймаутом



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



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

673   11  

11 ответов:

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

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

и вот отличный пост в блоге " Создание задачи.Метод TimeoutAfter " (от команды 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
}

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

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

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

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

Как насчет чего-то вроде этого?

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

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

здесь полностью работал пример, основанный на топ-проголосовали ответ, что:

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.

этот ответ действительно только применительно к 3-й партии библиотеки библиотеки, которые вы просто не можете выполнить рефакторинг, чтобы включить параметр timeout.

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

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

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

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

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

использовать таймер для обработки сообщения и автоматической отмены. Когда задача завершится, вызовите Dispose на таймеры, чтобы они никогда не срабатывали. Вот пример; измените 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);
                }
            }
        }
    }
}

и асинхронный CTP предоставляет TaskEx.Метод задержки, который обернет таймеры в задачи для вас. Это может дать вам больше контроля, чтобы делать такие вещи, как установить 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;
}

другой способ решения этой проблемы - использование реактивных расширений:

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

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

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;

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

TimeSpan timeout = TimeSpan.FromSeconds(10);

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

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

общая версия ответа @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();
}

кстати: когда тайм-аут происходит, тайм-аут исключение будет брошено

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

и хотя Джо Хог создает задачу.Метод 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;

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

Comments

    Ничего не найдено.