Асинхронно ждет задания в комплекте с таймаутом

StackOverflow https://stackoverflow.com/questions/4238345

  •  26-09-2019
  •  | 
  •  

Вопрос

Я хочу подождать Задачаu003CT> Для завершения некоторых специальных правил: если он не завершился после X Milliseconds, я хочу отобразить сообщение для пользователя. И если он не закончил после y миллисекунд, я хочу автоматически Отмена запроса.

я могу использовать Task.continuewith Для асинхронного ждать выполнения задачи (т. Е. График действия для выполнения, когда задача завершена), но это не позволяет указывать время ожидания. я могу использовать Задача Чтобы синхронно ждать, пока задача в комплекте с тайменами, но это блокирует мою нить. Как я могу асинхронно ждать, пока задача будет в комплекте с тайменами?

Это было полезно?

Решение

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

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

И вот Отличный блог пост «создание метода задания..

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

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 в комментарии к его ответ.

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

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

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

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.

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

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

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

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

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

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

Использовать Таймер обрабатывать сообщение и автоматическое отмену. Когда задача завершается, позвоните утилизируйте таймеры, чтобы они никогда не пожали. Вот пример; Измените 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, который будет обернуть таймеры в задачах для вас. Это может дать вам больше контроля, чтобы сделать такие вещи, как настроить задачCheduler для продолжения, когда таймер пожали.

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

Использование Стивена Очисти Asyncex. Библиотека, вы можете сделать:

TimeSpan timeout = TimeSpan.FromSeconds(10);

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

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

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

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;

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

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

Если вы используете блокировкиCollection, чтобы запланировать задачу, производитель может запускать потенциально длительную рабочую задачу, и потребитель может использовать метод Thribake, который содержит токен Timeout и Takellation Token.

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

  • В дополнение к Ответ Лоуренса, он отменяет исходную задачу, когда возникает время ожидания.
  • В приложении к Варианты ответа 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();
        }
    }
}

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

  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.

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

И хотя Crafting Joe Hoag's Crafted.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;
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top