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

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

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

22 ответа

Решение

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

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

А вот отличное сообщение в блоге "Создание метода Task.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.");
        }
    }
}

Начиная с .Net 6 (предварительная 7) или новее, для этого есть новый встроенный метод версияTask.WaitAsync .

      // Using TimeSpan
await myTask.WaitAsync(TimeSpan.FromSeconds(10));

// Using CancellationToken
await myTask.WaitAsync(cancellationToken);

// Using both TimeSpan and CancellationToken
await myTask.WaitAsync(TimeSpan.FromSeconds(10), cancellationToken);

Ты можешь использовать Task.WaitAny ждать первого из нескольких заданий.

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

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

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

Применение

InnerCallAsync может занять много времени. CallAsync оборачивает его таймаутом.

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

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

TimeSpan timeout = TimeSpan.FromSeconds(10);

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

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

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

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 секунд), затем оно либо отобразило бы ошибку и продолжило бы работать, либо повторило попытку.

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

    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, не блокируя основной поток, используя другую задачу.

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

Кроме того, 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;
}

С .Net 6 (предварительный просмотр 7 в качестве даты этого ответа) можно использовать новый WaitAsync (TimeSpan, CancellationToken), который отвечает этой конкретной потребности. Если вы можете использовать .Net6, эта версия, кроме того, описывается как оптимизированная по сравнению с большинством хороших решений, предлагаемых в этих сообщениях.

(Спасибо всем участникам, потому что я использовал ваше решение в течение многих лет)

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

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 выше с Reactive Extensions.

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 расширение OnTimeout. По истечении времени ожидания Task выполняет желаемое встроенное лямбда-действие Action() и возвращает true, в противном случае - false.

      public static async Task<bool> OnTimeout<T>(this T t, Action<T> action, int waitms) where T : Task
{
    if (!(await Task.WhenAny(t, Task.Delay(waitms)) == t))
    {
        action(t);
        return true;
    } else {
        return false;
    }
}

Расширение OnTimeout возвращает результат типа bool, который можно присвоить переменной, как в этом примере, вызывая асинхронный вызов UDP-сокета:

      var t = UdpSocket.ReceiveAsync();

var timeout = await t.OnTimeout(task => {
    Console.WriteLine("No Response");
}, 5000);

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

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

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

      public static async Task<TResult> WithTimeout<TResult>(this Task<TResult> task, TimeSpan timeout)
{
    if (await Task.WhenAny(task, Task.Delay(timeout)) != task)
        throw new TimeoutException();
    return await task;
}

Так что это древний вариант, но есть гораздо лучшее современное решение. Не уверен, какая версия C#/.NET требуется, но я это делаю так:


... Other method code not relevant to the question.

// a token source that will timeout at the specified interval, or if cancelled outside of this scope
using var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, timeoutTokenSource.Token);

async Task<MessageResource> FetchAsync()
{
    try
    {
        return await MessageResource.FetchAsync(m.Sid);
    } catch (TaskCanceledException e)
    {
        if (timeoutTokenSource.IsCancellationRequested)
            throw new TimeoutException("Timeout", e);
        throw;
    }
}

return await Task.Run(FetchAsync, linkedTokenSource.Token);

в CancellationTokenSource конструктор принимает TimeSpanпараметр, который приведет к отмене этого токена по истечении этого интервала. Затем вы можете обернуть свой асинхронный (или синхронный, если на то пошло) код в другой вызовTask.Run, передав токен тайм-аута.

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

Затем мы просто ловим OperationCancelledException и проверьте, какой токен вызвал исключение, и бросьте TimeoutExceptionесли тайм-аут вызвал его повышение. Иначе перебрасываем.

Кроме того, здесь я использую локальные функции, которые были введены в C# 7, но вы можете легко использовать лямбда-выражения или фактические функции для того же эффекта. Точно так же в C# 8 был введен более простой синтаксис для операторов using, но его достаточно легко переписать.

Я чувствовал 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;

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

  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

Вот низкоуровневая реализация метода, который принимает как тайм-аут, так и CancellationToken, а в случае исключения распространяет все ошибки Task<T>, вместо первой ошибки:

      public static Task<TResult> WaitAsync<TResult>(this Task<TResult> task,
    TimeSpan timeout, CancellationToken cancellationToken = default)
{
    if (task == null) throw new ArgumentNullException(nameof(task));
    if (timeout < TimeSpan.Zero && timeout != Timeout.InfiniteTimeSpan)
        throw new ArgumentOutOfRangeException(nameof(timeout));

    var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
    cts.CancelAfter(timeout);

    return task
        .ContinueWith(_ => { }, cts.Token,
            TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default)
        .ContinueWith(continuation =>
        {
            cts.Dispose();
            if (task.IsCompleted) return task;
            cancellationToken.ThrowIfCancellationRequested();
            if (continuation.IsCanceled) throw new TimeoutException();
            return task;
        }, TaskScheduler.Default).Unwrap();
}

А TimeoutException выбрасывается, если тайм-аут истекает до завершения task.

Честно говоря, распространение всех ошибок на самом деле не является функцией добавления ценности в этом случае. Причина в том, что если вы используете вот так: await someTask.WaitAsync(timeout), любые лишние ошибки будут поглощены awaitв любом случае, который по замыслу распространяет только первое исключение из ожидаемой задачи. И нет особого смысла хранить WaitAsync задачу в переменной и исследуем ее внутри catch блок, потому что у вас уже есть someTask доступен, и вы можете изучить это вместо этого.

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

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

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

На случай, если кто-то ищет что-то подобное (через 12 лет после вопроса ОП)..

Другой вариант — просто использовать Task.Wait(timeout) внутри другого Task.Run(). Это если вы хотите каким-то образом избежать использования Task.WaitAny() или даже вызова ожидания. Или, в моем случае, просто для согласованности с остальными файлами .cs, с которыми я работаю.

Что-то вроде этого:

              int timeout = 5000;
        var actualTask = new Task(() =>
        {
            // Do your stuff here
        });

        Task.Run(() =>
        {
            actualTask.Start();
            if (!actualTask.Wait(timeout))
            {
                return false;
                // or throw new TimeoutException("Operation timed out!");
            }

            return true;
        }).ContinueWith((timedTaskResult) =>
        {
            if (!timedTaskResult.Result)
            {
                // tell user it timed out!
            }

            if (timedTaskResult.IsFaulted)
            {
                // Tell the user about the error/s via the timedTaskResult.Exception
            }
        });

Определенно не делайте этого, но это вариант, если... Я не могу придумать вескую причину.

((CancellationTokenSource)cancellationToken.GetType().GetField("m_source",
    System.Reflection.BindingFlags.NonPublic |
    System.Reflection.BindingFlags.Instance
).GetValue(cancellationToken)).Cancel();
Другие вопросы по тегам