Asynchronicznie poczekaj na zakończenie zadania <T>

387

Chcę poczekać na zakończenie zadania <T> z pewnymi specjalnymi regułami: jeśli nie zakończyło się ono po X milisekundach, chcę wyświetlić wiadomość dla użytkownika. A jeśli nie zakończy się po upływie milisekund Y, chcę automatycznie poprosić o anulowanie .

Mogę użyć Task.ContinueWith, aby asynchronicznie czekać na zakończenie zadania (tj. Zaplanować wykonanie akcji po zakończeniu zadania), ale to nie pozwala określić limitu czasu. Mogę użyć Task.Wait, aby synchronicznie czekać na zakończenie zadania z limitem czasu, ale to blokuje mój wątek. Jak asynchronicznie czekać na zakończenie zadania z limitem czasu?

dtb
źródło
3
Masz rację. Dziwi mnie, że nie przewiduje to limitu czasu. Być może w .NET 5.0 ... Oczywiście możemy wbudować limit czasu w samo zadanie, ale to nie jest dobre, takie rzeczy muszą się uwolnić.
Aliostad
4
Chociaż nadal wymagałoby to logiki dla opisanego dwupoziomowego limitu czasu, .NET 4.5 rzeczywiście oferuje prostą metodę tworzenia opartego na limicie czasu CancellationTokenSource. Dostępne są dwa przeciążenia konstruktora, jedno z opóźnieniem całkowitym o milisekundę, a drugie z opóźnieniem TimeSpan.
patridge
Kompletne proste źródło lib tutaj: stackoverflow.com/questions/11831844/…
jakieś ostateczne rozwiązanie z pełnym kodem źródłowym działającym? może bardziej złożona próbka do zgłaszania błędów w każdym wątku i po WaitAll wyświetla podsumowanie?
Kiquenet

Odpowiedzi:

563

Co powiesz na to:

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

A oto świetny wpis na blogu „Crafting a Task.TimeoutAfter Method” (od zespołu MS Parallel Library) z dodatkowymi informacjami na ten temat .

Dodanie : na prośbę o komentarz do mojej odpowiedzi, oto rozszerzone rozwiązanie obejmujące obsługę anulowania. Pamiętaj, że przekazanie anulowania do zadania i timera oznacza, że ​​w kodzie może wystąpić anulowanie na wiele sposobów, dlatego powinieneś sprawdzić i upewnić się, że poprawnie wykonałeś wszystkie z nich. Nie pozostawiaj przypadkowi różnych kombinacji i miej nadzieję, że komputer działa prawidłowo w czasie wykonywania.

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
źródło
86
Należy wspomnieć, że chociaż Task.Delay może ukończyć zadanie przed długim uruchomieniem, umożliwiając obsłużenie scenariusza przekroczenia limitu czasu, NIE anuluje ono samego zadania trwającego długo; WhenAny po prostu informuje, że jedno z przekazanych zadań zostało zakończone. Musisz samodzielnie wdrożyć CancellationToken i anulować długo trwające zadanie.
Jeff Schumacher
30
Można również zauważyć, że Task.Delayzadanie jest wspierane przez zegar systemowy, który będzie śledzony aż do upływu limitu czasu, niezależnie od tego, jak długo SomeOperationAsyncto potrwa. Więc jeśli ten ogólny fragment kodu wykonuje się często w ciasnej pętli, zużywasz zasoby systemowe dla liczników czasu, aż upłynie limit czasu. Sposobem na rozwiązanie tego problemu jest CancellationTokenprzekazanie go do Task.Delay(timeout, cancellationToken)anulowania po SomeOperationAsynczakończeniu w celu zwolnienia zasobu timera.
Andrew Arnott,
12
Kod anulowania wykonuje zbyt wiele pracy. Spróbuj tego: int timeout = 1000; var cancellationTokenSource = new CancellationTokenSource (limit czasu); var cancellationToken = tokenSource.Token; var task = SomeOperationAsync (cancellationToken); spróbuj {czekaj na zadanie; // Dodaj kod tutaj dla pomyślnego zakończenia} catch (OperationCancelledException) {// Dodaj kod tutaj w przypadku
przekroczenia
3
@ilans, oczekując Task, każdy wyjątek przechowywany przez zadanie zostanie w tym momencie ponownie zgłoszony. Daje to szansę na złapanie OperationCanceledException(jeśli zostanie anulowane) lub inny wyjątek (jeśli nastąpi błąd).
Andrew Arnott,
3
@TomexOu: pytanie brzmiało, jak asynchronicznie oczekiwać na zakończenie zadania. Task.Wait(timeout)synchronicznie blokowałby zamiast asynchronicznie oczekiwać.
Andrew Arnott
220

Oto wersja metody rozszerzenia, która obejmuje anulowanie limitu czasu po zakończeniu pierwotnego zadania, zgodnie z sugestią Andrew Arnotta w komentarzu do jego odpowiedzi .

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.");
        }
    }
}
Lawrence Johnston
źródło
8
Daj temu człowiekowi kilka głosów. Eleganckie rozwiązanie. A jeśli telefon nie ma typu zwrotu, upewnij się, że po prostu usunąłeś TResult.
Lucas,
6
CancellationTokenSource jest jednorazowego użytku i powinien znajdować się w usingbloku
PeterM
6
@ It'satrap Oczekiwanie na zadanie dwa razy po prostu zwraca wynik za drugim oczekiwaniem. Nie wykonuje się dwa razy. Można powiedzieć, że jest równy, task.Result gdy zostanie wykonany dwukrotnie.
M. Mimpen,
7
Czy oryginalne zadanie ( task) nadal będzie działać w przypadku przekroczenia limitu czasu?
jag
6
Możliwość drobnej poprawy: TimeoutExceptionma odpowiedni domyślny komunikat. Przesłonięcie go przez „Upłynął limit czasu operacji”. nie dodaje żadnej wartości i faktycznie powoduje pewne zamieszanie, sugerując, że istnieje powód, aby ją zastąpić.
Edward Brey,
49

Możesz użyć, Task.WaitAnyaby poczekać na pierwsze z wielu zadań.

Możesz utworzyć dwa dodatkowe zadania (które zostaną ukończone po upływie określonych limitów czasu), a następnie użyć, WaitAnyaby poczekać na to, co zakończy się jako pierwsze. Jeśli zadanie, które zostało ukończone jako pierwsze, jest Twoim zadaniem „roboczym”, to gotowe. Jeśli zadanie, które zostało ukończone jako pierwsze, jest limitem czasu, możesz zareagować na limit czasu (np. Anulowanie żądania).

Tomas Petricek
źródło
1
Widziałem tę technikę stosowaną przez MVP, którą naprawdę szanuję, wydaje mi się ona znacznie czystsza niż zaakceptowana odpowiedź. Być może przykład pomoże zdobyć więcej głosów! Zrobiłbym to, ale nie mam wystarczającego doświadczenia w
zadaniu,
3
jeden wątek zostałby zablokowany - ale jeśli nie masz nic przeciwko, to nie ma problemu. Rozwiązanie, które wybrałem, było poniższe, ponieważ żadne wątki nie są blokowane. Przeczytałem post na blogu, który był naprawdę dobry.
JJschk
@JJschk wspomniałeś, że wybrałeś rozwiązanie below... co to jest? na podstawie zamówienia SO?
BozoJoe
a co jeśli nie chcę anulować wolniejszego zadania? Chcę sobie z tym poradzić, gdy zakończy się, ale wróć z obecnej metody.
Akmal Salikhov
18

Co powiesz na coś takiego?

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

Możesz użyć opcji Task.Wait bez blokowania głównego wątku za pomocą innego zadania.

as-cii
źródło
W rzeczywistości w tym przykładzie nie czekasz wewnątrz t1, ale na wyższym zadaniu. Spróbuję zrobić bardziej szczegółowy przykład.
as-cii
14

Oto w pełni działający przykład oparty na najczęściej głosowanej odpowiedzi, czyli:

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

Główną zaletą implementacji w tej odpowiedzi jest to, że dodano ogólne, więc funkcja (lub zadanie) może zwrócić wartość. Oznacza to, że każdą istniejącą funkcję można zawinąć w funkcję limitu czasu, np .:

Przed:

int x = MyFunc();

Po:

// Throws a TimeoutException if MyFunc takes more than 1 second
int x = TimeoutAfter(MyFunc, TimeSpan.FromSeconds(1));

Ten kod wymaga .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();
            }
        }
    }
}

Ostrzeżenia

Po udzieleniu tej odpowiedzi generalnie nie jest dobrą praktyką zgłaszanie wyjątków w kodzie podczas normalnej pracy, chyba że absolutnie musisz:

  • Za każdym razem, gdy zgłaszany jest wyjątek, jest to niezwykle ciężka operacja,
  • Wyjątki mogą spowolnić kod o współczynnik 100 lub więcej, jeśli wyjątki są w ścisłej pętli.

Użyj tego kodu tylko wtedy, gdy absolutnie nie możesz zmienić funkcji, którą wywołujesz, więc upłynie limit czasu po określonym TimeSpan .

Ta odpowiedź ma zastosowanie tylko w przypadku bibliotek bibliotek stron trzecich, których po prostu nie można refaktoryzować w celu włączenia parametru limitu czasu.

Jak napisać solidny kod

Jeśli chcesz napisać solidny kod, ogólna zasada jest następująca:

Każda operacja, która potencjalnie może zostać zablokowana na czas nieokreślony, musi mieć limit czasu.

Jeśli nie zastosujesz się do tej reguły, Twój kod w końcu uderzy w operację, która z jakiegoś powodu zakończy się niepowodzeniem, a następnie zablokuje się na czas nieokreślony, a aplikacja właśnie zawiesiła się na stałe.

Jeśli po pewnym czasie nastąpił rozsądny limit czasu, aplikacja zawiesiłaby się na bardzo długi czas (np. 30 sekund), a następnie albo wyświetli błąd i będzie kontynuowała swoją wesołą drogę, albo spróbuje ponownie.

Contango
źródło
11

Korzystając z doskonałej biblioteki AsyncEx Stephena Cleary'ego , możesz:

TimeSpan timeout = TimeSpan.FromSeconds(10);

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

TaskCanceledException zostanie wyrzucony w przypadku przekroczenia limitu czasu.

Cocowalla
źródło
10

To jest nieco ulepszona wersja poprzednich odpowiedzi.

  • Oprócz odpowiedzi Lawrence'a anuluje pierwotne zadanie, gdy nastąpi przekroczenie limitu czasu.
  • Oprócz wariantów odpowiedzi 2 i 3 sjb , możesz przewidzieć CancellationTokenoryginalne zadanie, a gdy nastąpi przekroczenie limitu czasu, otrzymasz TimeoutExceptionzamiast 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();
        }
    }
}

Stosowanie

InnerCallAsyncjego ukończenie może zająć dużo czasu. CallAsyncowija to limitem czasu.

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;
}
Josef Bláha
źródło
1
Dzięki za rozwiązanie! Wygląda na to, należy przejść timeoutCancellationdo delayTask. Obecnie, jeśli anulujesz ostrzał, CancelAfterAsyncmożesz rzucić TimeoutExceptionzamiast TaskCanceledException, bo przyczyną delayTaskmoże być pierwsza.
AxelUser
@AxelUser, masz rację. Zrozumienie tego, co się działo, zajęło mi godzinę z testami jednostkowymi :) Zakładałem, że gdy oba podane zadania WhenAnyzostaną anulowane tym samym tokenem, WhenAnyzwróci pierwsze zadanie. To założenie było błędne. Zredagowałem odpowiedź. Dzięki!
Josef Bláha
Trudno mi się dowiedzieć, jak to nazwać za pomocą zdefiniowanej funkcji Task <SomeResult>; jakąkolwiek szansę, byś mógł podać przykład, jak to nazwać?
jhaagsma
1
@jhaagsma, dodano przykład!
Josef Bláha
@ JosefBláha Dzięki bardzo! Wciąż powoli owijam głowę wokół składni stylu lambda, co nie przyszło mi do głowy - że token jest przekazywany do zadania w treści CancelAfterAsync, przekazując funkcję lambda. Sprytne!
jhaagsma
8

Użyj timera, aby obsłużyć wiadomość i automatyczne anulowanie. Po zakończeniu zadania zadzwoń po zegar na Dispose, aby nigdy nie strzelały. Oto przykład; zmień zadanie Opóźnij do 500, 1500 lub 2500, aby zobaczyć różne przypadki:

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

Ponadto Async CTP udostępnia metodę TaskEx.Delay, która zawija timery w zadaniach dla Ciebie. Może to dać ci większą kontrolę nad takimi czynnościami, jak ustawianie TaskScheduler dla kontynuacji po uruchomieniu Timera.

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;
}
Kwatermistrz
źródło
Nie chce, aby bieżący wątek był blokowany, to znaczy nie task.Wait().
Cheng Chen,
@Danny: To tylko po to, by uzupełnić przykład. Po kontynuacji możesz wrócić i uruchomić zadanie. Zaktualizuję moją odpowiedź, aby była bardziej przejrzysta.
kwatermistrz
2
@dtb: Co zrobić, jeśli uczynisz z t1 Zadanie <Zadanie <Wynik>>, a następnie wywołasz TaskExtensions.Unwrap? Możesz zwrócić t2 z wewnętrznej lambda, a następnie możesz dodać kontynuacje do nieopakowanego zadania.
kwatermistrz
Niesamowite! To doskonale rozwiązuje mój problem. Dzięki! Myślę, że pójdę z rozwiązaniem zaproponowanym przez @ AS-CII, chociaż chciałbym również przyjąć twoją odpowiedź za zasugerowanie rozszerzenia zadań. Rozwiń Czy mogę otworzyć nowe pytanie, abyś mógł uzyskać przedstawiciela, na który zasługujesz?
dtb
6

Innym sposobem rozwiązania tego problemu jest użycie rozszerzeń reaktywnych:

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

Przetestuj powyżej, używając poniższego kodu w teście jednostkowym, działa dla mnie

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

Może być potrzebny następujący obszar nazw:

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
źródło
4

Ogólna wersja powyższej odpowiedzi @ Kevana, wykorzystująca rozszerzenia reaktywne.

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

Z opcjonalnym harmonogramem:

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: Kiedy dojdzie do przekroczenia limitu czasu, zostanie zgłoszony wyjątek przekroczenia limitu czasu

Jasper H Bojsen
źródło
0

Jeśli używasz BlockingCollection do zaplanowania zadania, producent może uruchomić potencjalnie długo działające zadanie, a konsument może skorzystać z metody TryTake, która ma wbudowany token limitu czasu i anulowania.

kns98
źródło
Musiałbym coś napisać (nie chcę tutaj umieszczać zastrzeżonego kodu), ale scenariusz jest taki. Producent będzie kodem, który wykonuje metodę, która może przekroczyć limit czasu i po zakończeniu umieszcza wyniki w kolejce. Klient wywoła trytake () z limitem czasu i otrzyma token po przekroczeniu limitu czasu. Zarówno producent, jak i konsument wykonają zadania backround i wyświetlą komunikat dla użytkownika za pomocą narzędzia do wysyłania wątków interfejsu użytkownika, jeśli zajdzie taka potrzeba.
kns98
0

Czułem to Task.Delay()zadanie i CancellationTokenSourcew innych odpowiedziach trochę za mój przypadek użycia w ciasnej pętli sieciowej.

I choć Joe Hoag's Crafting a Task.Timeout Po tym, jak metoda na blogach MSDN była inspirująca, byłem trochę zmęczony używaniemTimeoutException kontroli przepływu z tego samego powodu, co powyżej, ponieważ przekroczenia limitu czasu są oczekiwane częściej niż nie.

Więc poszedłem z tym, który obsługuje również optymalizacje wspomniane na blogu:

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

Przykładowy przypadek użycia jest taki:

var receivingTask = conn.ReceiveAsync(ct);

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

// Read and do something with data
var data = await receivingTask;
antak
źródło
0

Kilka wariantów odpowiedzi Andrew Arnotta:

  1. Jeśli chcesz poczekać na istniejące zadanie i dowiedzieć się, czy zostało ono ukończone, czy upłynął limit czasu, ale nie chcesz go anulować, jeśli upłynie limit czasu:

    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. Jeśli chcesz rozpocząć zadanie robocze i anulować je, jeśli upłynie limit czasu:

    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. Jeśli masz już utworzone zadanie, które chcesz anulować w przypadku przekroczenia limitu czasu:

    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
    }

Kolejny komentarz, te wersje anulują licznik czasu, jeśli limit czasu nie wystąpi, więc wiele wywołań nie spowoduje gromadzenia się liczników.

sjb

sjb-sjb
źródło
0

Ponownie łączę pomysły innych odpowiedzi tutaj i tej odpowiedzi w innym wątku w metodę rozszerzenia typu Try. Jest to korzystne, jeśli chcesz zastosować metodę rozszerzenia, ale unikając wyjątku po przekroczeniu limitu czasu.

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
źródło
-3

Zdecydowanie nie rób tego, ale jest to opcja, jeśli ... Nie mogę wymyślić ważnego powodu.

((CancellationTokenSource)cancellationToken.GetType().GetField("m_source",
    System.Reflection.BindingFlags.NonPublic |
    System.Reflection.BindingFlags.Instance
).GetValue(cancellationToken)).Cancel();
syko9000
źródło