Асинхронно зачекайте, коли завдання <T> завершиться з таймаутом


387

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

Я можу використовувати Task.ContinueWith для асинхронного очікування завершення завдання (тобто планувати дію, яку слід виконати, коли завдання виконано), але це не дозволяє вказати час очікування. Я можу використовувати Task.Будьте, щоб синхронно чекати завершення завдання з таймаутом, але це блокує мою нитку. Як я можу асинхронно чекати завершення завдання з таймаутом?


3
Ти правий. Я здивований, що це не передбачає тайм-ауту. Можливо, в .NET 5.0 ... Звичайно, ми можемо вкласти час у самому завданні, але це не добре, такі речі повинні стати безкоштовними.
Аліостад

4
Незважаючи на те, що все ще потрібна логіка для описаного вами дворівневого тайм-ауту, .NET 4.5 дійсно пропонує простий метод створення на основі тайм-ауту CancellationTokenSource. Доступні дві перевантаження конструктору: одна затримує цілу мілісекундну затримку, а друга - затримка TimeSpan.
патрон

Повний вихідний простий Lib тут: stackoverflow.com/questions/11831844 / ...

будь-яке остаточне рішення з повним вихідним кодом працює? можливо складніший зразок для повідомлення про помилки в кожному потоці і після того, як WaitAll показує підсумок?
Кікенет

Відповіді:


563

Як щодо цього:

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) з додатковою інформацією про подібні речі .

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

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 = новий CancellationTokenSource (таймаут); var cancellationToken = tokenSource.Token; var task = SomeOperationAsync (cancellationToken); спробуйте {очікувати завдання; // Додайте сюди код для успішного завершення} catch (OperationCancegledException) {// Додати код сюди для випадку очікування}
srm

3
@ilans, чекаючи Task, будь-який виняток, що зберігається завданням, буде повторно скинутий у цей момент. Це дає вам можливість спіймати OperationCanceledException(якщо скасовано) або будь-який інший виняток (якщо винен).
Ендрю Арнотт

3
@TomexOu: питання полягало в тому, як асинхронно чекати завершення завдання. Task.Wait(timeout)буде синхронно блокувати замість асинхронно очікувати.
Ендрю Арнотт

220

Ось версія методу розширення, яка включає скасування тайм-ауту, коли початкове завдання буде виконано, як запропонував Ендрю Арнотт у коментарі до своєї відповіді .

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продовжить виконання оригінального завдання ( ) у разі очікування?
джег

6
Незначна можливість вдосконалення: TimeoutExceptionмає відповідне повідомлення за замовчуванням. Переосмисливши його на "Операція минула". не додає ніякої цінності і насправді викликає певну плутанину, маючи на увазі, що є причина перекрити це.
Едвард Брей

49

Ви можете використовувати, Task.WaitAnyщоб чекати першого з декількох завдань.

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


1
Я бачив цю техніку, яку використовує МВП, яку я дуже поважаю, вона здається мені набагато чистішою, ніж прийнята відповідь. Можливо, приклад допоможе отримати більше голосів! Я б зголосився це зробити, за винятком у мене недостатнього досвіду роботи, щоб бути впевненим, що це буде корисно :)
GrahamMc

3
один потік буде заблокований - але якщо ур добре з цим, то ніяких проблем. Я прийняв рішення, яке було подано нижче, оскільки жодні потоки не заблоковані. Я прочитав повідомлення в блозі, яке було дуже добре.
JJschk

@JJschk ви згадуєте, що ви прийняли рішення below.... що це? на основі замовлення SO?
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, не блокуючи основний потік за допомогою іншого завдання.


Насправді в цьому прикладі ви не чекаєте всередині 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 буде кинуто у разі закінчення часу.


10

Це дещо вдосконалена версія попередніх відповідей.

  • Окрім відповіді Лоуренса , воно скасовує оригінальне завдання, коли настає час очікування.
  • На додаток до варіантів відповідей 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;
}

1
Дякую за рішення! Схоже , ви повинні пройти timeoutCancellationв delayTask. Наразі, якщо ви скасуєте пожежу, CancelAfterAsyncможе TimeoutExceptionзамість цього кинути TaskCanceledException, причина delayTaskможе закінчитися першою.
AxelUser

@AxelUser, ти маєш рацію. Знадобилося мені годину з купою тестових одиниць, щоб зрозуміти, що відбувається :) Я припускав, що коли обидва завдання WhenAny, відмінені одним і тим же маркером, WhenAnyповерне перше завдання. Це припущення було помилковим. Я відредагував відповідь. Дякую!
Йозеф Блаха

Мені важко зрозуміти, як насправді викликати це за допомогою визначеної функції Завдання <SomeResult>; будь-який шанс ви могли використати на прикладі, як це назвати?
jhaagsma

1
@jhaagsma, приклад додано!
Йозеф Блаха

@ JosefBláha Дуже дякую! Я все ще повільно обмотаю голову навколо синтаксису стилю лямбда, що б мені не прийшло в голову - що маркер передається завданням в тілі CancelAfterAsync, передаючи функцію лямбда. Вишуканий!
jhaagsma

8

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

Він не хоче, щоб поточна нитка була заблокована, тобто ні task.Wait().
Чен Чен

@Danny: Це було лише для того, щоб зробити приклад завершеним. Після ContinueWith ви можете повернутися і дозволити виконувати завдання. Я оновлю свою відповідь, щоб зробити це більш зрозумілим.
Кварталмайстер

2
@dtb: Що робити, якщо ви зробите t1 a Завдання <Завдання <Результат>>, а потім викликаєте TaskExtensions.Unwrap? Ви можете повернути t2 зі своєї внутрішньої лямбда, а потім можете додати продовження до розгорнутого завдання.
Кварталмайстер

Дивовижно! Це прекрасно вирішує мою проблему. Дякую! Думаю, я піду з рішенням, запропонованим @ AS-CII, хоча я б хотів, щоб я міг також прийняти вашу відповідь, що я запропонував TaskExtensions.Unwrap Потрібно відкрити нове запитання, щоб ви могли отримати відповідь, яку ви заслужили?
dtb

6

Ще один спосіб вирішення цієї проблеми - використання реактивних розширень:

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;

4

Узагальнена версія відповіді @ Кевана вище з використанням реактивних розширень.

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

BTW: Коли час очікування відбудеться, буде викинуто виняток із таймауту


0

Якщо ви використовуєте BlockingCollection для планування завдання, виробник може запустити потенційно тривале завдання, а споживач може використовувати метод TryTake, в який вбудований маркер очікування та скасування.


Мені довелося б щось написати (не хочу тут ставити власний код), але сценарій такий. Виробником буде код, який виконує метод, який міг би вимкнути час і поставить результати у чергу після завершення. Споживач зателефонує trytake () із затримкою та отримає маркер після таймауту. Як виробник, так і споживач виконуватимуть завдання "backround" та відображатимуть повідомлення користувачу, використовуючи диспетчер потоку інтерфейсу користувача, якщо це буде необхідно.
kns98

0

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

І хоча Joe Hoag створив завдання Task.TimeoutAfter в блогах MSDN надихав, я трохи втомлювався використовувати TimeoutExceptionдля управління потоком з тієї ж причини, що і вище, тому що очікування очікується частіше, ніж ні.

Тому я пішов із цим, що також займається оптимізаціями, згаданими в блозі:

public static async Task<bool> BeforeTimeout(this Task task, int millisecondsTimeout)
{
    if (task.IsCompleted) return true;
    if (millisecondsTimeout == 0) return false;

    if (millisecondsTimeout == Timeout.Infinite)
    {
        await Task.WhenAll(task);
        return true;
    }

    var tcs = new TaskCompletionSource<object>();

    using (var timer = new Timer(state => ((TaskCompletionSource<object>)state).TrySetCanceled(), tcs,
        millisecondsTimeout, Timeout.Infinite))
    {
        return await Task.WhenAny(task, tcs.Task) == task;
    }
}

Приклад використання:

var receivingTask = conn.ReceiveAsync(ct);

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

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

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


0

Я рекомендую ідеї деяких інших відповідей тут, і цю відповідь на іншій нитці в метод спробу розширення стилю. Це має перевагу, якщо потрібно метод розширення, але уникаючи винятку після закінчення часу.

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

-3

Однозначно не робіть цього, але це варіант, якщо ... я не можу придумати поважну причину.

((CancellationTokenSource)cancellationToken.GetType().GetField("m_source",
    System.Reflection.BindingFlags.NonPublic |
    System.Reflection.BindingFlags.Instance
).GetValue(cancellationToken)).Cancel();
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.