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

388

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

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

DTB
источник
3
Вы правы. Я удивлен, что это не обеспечивает тайм-аут. Может быть, в .NET 5.0 ... Конечно, мы можем встроить тайм-аут в саму задачу, но это бесполезно, такие вещи должны быть бесплатными.
Алиостад
4
Хотя для описываемого вами двухуровневого тайм-аута все еще требуется логика, в .NET 4.5 действительно существует простой метод создания тайм-аутов CancellationTokenSource. Доступны две перегрузки для конструктора, одна из которых принимает целочисленную миллисекундную задержку, а другая - задержку TimeSpan.
Патридж
Полный простой исходный код lib здесь: stackoverflow.com/questions/11831844/…
любое окончательное решение с полным исходным кодом работает? может быть, более сложный пример для уведомлений об ошибках в каждом потоке и после WaitAll показывает сводку?
Kiquenet

Ответы:

566

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

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
}
Эндрю Арнотт
источник
86
Следует отметить, что, хотя Task.Delay может завершиться до длительного выполнения задачи, что позволяет вам обрабатывать сценарий тайм-аута, это НЕ отменяет саму длительную задачу; WhenAny просто сообщает вам, что одна из задач, переданных ему, выполнена. Вам нужно будет реализовать CancellationToken и отменить долгосрочное задание самостоятельно.
Джефф Шумахер
30
Также можно отметить, что Task.Delayзадание поддерживается системным таймером, который будет отслеживаться до истечения времени ожидания независимо от того, сколько времени SomeOperationAsyncзанимает. Таким образом, если весь этот фрагмент кода выполняется много раз в узком цикле, вы расходуете системные ресурсы на таймеры, пока они не прекратят работу. Способ исправить это состоит в том, CancellationTokenчто вы передадите ему Task.Delay(timeout, cancellationToken)то, что отмените, когда SomeOperationAsyncзакончите, чтобы освободить ресурс таймера.
Эндрю Арнотт
12
Код отмены делает слишком много работы. Попробуйте это: int timeout = 1000; var cancellationTokenSource = new CancellationTokenSource (время ожидания); var cancellationToken = tokenSource.Token; var task = SomeOperationAsync (cancellationToken); попробуй {жди задание; // Добавить код здесь для успешного завершения} catch (OperationCancelledException) {// Добавить код здесь для случая таймаута}
srm
3
@ilans, ожидая Task, что любое исключение, сохраненное задачей, перебрасывается в этот момент. Это дает вам шанс поймать OperationCanceledException(если отменено) или любое другое исключение (если отказано).
Эндрю Арнотт
3
@TomexOu: вопрос заключался в том, как асинхронно ожидать завершения задачи. Task.Wait(timeout)будет синхронно блокировать вместо асинхронного ожидания.
Эндрю Арнотт
221

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

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.");
        }
    }
}
Лоуренс Джонстон
источник
8
Дайте этому человеку несколько голосов. Элегантное решение. И если ваш вызов не имеет типа возврата, убедитесь, что вы просто удалили TResult.
Лукас
6
CancellationTokenSource является одноразовым и должен быть в usingблоке
PeterM
6
@ Itatrap Ожидание задачи дважды просто возвращает результат второго ожидания. Это не выполняется дважды. Вы могли бы сказать, что это равняется, task.Result когда выполнено дважды.
М. Мимпен
7
Будет ли исходная задача ( task) все еще продолжаться в случае тайм-аута?
Jag
6
Небольшая возможность улучшения: TimeoutExceptionесть подходящее сообщение по умолчанию. Переопределение с помощью «Время операции истекло». не добавляет никакой ценности и фактически вызывает некоторую путаницу, подразумевая, что есть причина переопределить это.
Эдвард Брей
49

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

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

Томас Петричек
источник
1
Я видел эту технику, которую использует MVP, которую я действительно уважаю, она кажется мне намного чище, чем принятый ответ. Возможно, пример поможет получить больше голосов! Я бы добровольно это сделал, за исключением того, что у меня недостаточно опыта в
заданиях,
3
один поток будет заблокирован - но если вы согласны с этим, то нет проблем. Решение, которое я выбрал, было ниже, так как никакие темы не заблокированы. Я прочитал пост в блоге, который был действительно хорош.
JJschk
@JJschk вы упомянули, что приняли решение below.... что это? на основе ТАКОГО заказа?
BozoJoe
а что если я не хочу отменять более медленную задачу? Я хочу справиться с этим, когда он закончится, но вернусь из текущего метода ..
Акмаль Салихов
18

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

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

в-CII
источник
На самом деле в этом примере вы не ожидаете внутри t1, а выполняете более высокую задачу. Я постараюсь сделать более подробный пример.
as-cii
14

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

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

Контанго
источник
11

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

TimeSpan timeout = TimeSpan.FromSeconds(10);

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

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

Cocowalla
источник
10

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

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;
}
Йозеф Блаха
источник
1
Спасибо за решение! Похоже, вы должны перейти timeoutCancellationв delayTask. В настоящее время, если вы запускаете отмену, CancelAfterAsyncможете бросить TimeoutExceptionвместо TaskCanceledException, причина delayTaskможет закончиться первой.
AxelUser
@AxelUser, ты прав. Мне потребовался час с кучей юнит-тестов, чтобы понять, что происходит :) Я предположил, что, когда обе задачи, которые были даны WhenAnyодним и тем же токеном, WhenAnyвернут первую задачу. Это предположение было неверным. Я отредактировал ответ. Спасибо!
Йозеф Блаха
Мне трудно понять, как на самом деле вызвать это с помощью определенной функции Task <SomeResult>; есть ли у вас шанс привести пример того, как это назвать?
Джаагсма
1
@jhaagsma, пример добавлен!
Йозеф Блаха
@ JosefBláha Большое спасибо! Я все еще медленно оборачиваюсь вокруг синтаксиса лямбда-стиля, который бы мне не приходил в голову - что токен передается задаче в теле CancelAfterAsync путем передачи лямбда-функции. Острота!
Джаагсма
8

Используйте таймер для обработки сообщения и автоматической отмены. Когда Задача завершится, вызовите 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;
}
Quartermeister
источник
Он не хочет, чтобы текущий поток был заблокирован, то есть нет task.Wait().
Ченг Чен
@ Дэнни: Это было только для того, чтобы сделать пример завершенным. После продолжения вы можете вернуться и запустить задачу. Я обновлю свой ответ, чтобы сделать это более ясным.
Quartermeister
2
@dtb: Что если вы сделаете t1 Задачей <Task <Result >>, а затем вызовите TaskExtensions.Unwrap? Вы можете вернуть t2 из своей внутренней лямбды, а затем можете добавить продолжения к развернутой задаче.
Квартмейстер
Потрясающие! Это прекрасно решает мою проблему. Спасибо! Я думаю, что я пойду с решением, предложенным @ AS-CII, хотя мне бы хотелось, чтобы я также принял ваш ответ за предложение TaskExtensions.Unwrap Должен ли я открыть новый вопрос, чтобы вы могли получить представителя, которого заслуживаете?
ДТБ
6

Другой способ решения этой проблемы - использование 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
источник
4

Общая версия ответа @ 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 is null 
       ? task.ToObservable().Timeout(timeout).ToTask() 
       : task.ToObservable().Timeout(timeout, scheduler).ToTask();
}

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

Джаспер Бойсен
источник
0

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

kns98
источник
Я должен был бы что-то написать (не хочу, чтобы здесь был проприетарный код), но сценарий такой. Производителем будет код, который выполняет метод, который может истечь, и по окончании поместит результаты в очередь. Потребитель вызовет trytake () с тайм-аутом и получит токен по истечении тайм-аута. Как производитель, так и потребитель будут выполнять фоновые задачи и отображать сообщение пользователю, используя диспетчер потоков пользовательского интерфейса, если это будет необходимо.
kns98
0

Я почувствовал 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;
antak
источник
0

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

  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

SJB-SJB
источник
0

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

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);
    }
}    
TM1
источник
-3

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

((CancellationTokenSource)cancellationToken.GetType().GetField("m_source",
    System.Reflection.BindingFlags.NonPublic |
    System.Reflection.BindingFlags.Instance
).GetValue(cancellationToken)).Cancel();
syko9000
источник