Asynchronicznie czekać na zakończenie zadania z timeoutem

Chcę poczekać na zadanie, aby dopełnić pewne specjalne zasady: Jeśli nie zakończy się po milisekundach X, chcę wyświetlić wiadomość użytkownikowi. A jeśli nie zakończy się po milisekundach Y, chcę automatycznie zażądać anulowania .

Mogę użyć zadania.Kontynuuj , aby asynchronicznie czekać na zakończenie zadania( tzn. zaplanować akcję do wykonania po zakończeniu zadania), ale to nie pozwala określić limitu czasu. Mogę użyć zadanie.Poczekaj aby synchronicznie czekać na zakończenie zadania z limitem czasu, ale to blokuje mój wątek. Jak mogę asynchronicznie czekać na zakończenie zadania z limitem czasu?

Author: dtb, 2010-11-21

11 answers

A może tak:

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 " Tworzenie zadania.TimeoutAfter Method " (od zespołu bibliotek MS Parallel) z Więcej informacji na ten temat.

Dodatek: na prośbę komentarza do mojej odpowiedzi, oto rozszerzone rozwiązanie, które obejmuje obsługę anulowania. Pamiętaj, że przekazanie anulowania do zadania i timera oznacza, że istnieje wiele sposobów anulowania w Twoim kodzie, i powinieneś mieć pewność, że przetestujesz dla i mieć pewność, że prawidłowo obsłużyć wszystkie z nich. Nie pozostawiaj przypadkowych kombinacji i miej nadzieję, że Twój komputer zrobi to, co należy 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
}
 428
Author: Andrew Arnott,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-19 05:30:21

Oto wersja metody rozszerzenia, która zawiera anulowanie limitu czasu, gdy oryginalne zadanie zostanie ukończone, 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.");
        }
    }
}
 138
Author: Lawrence Johnston,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-22 22:07:03

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

Możesz utworzyć dwa dodatkowe zadania (które kończą się po określonym czasie), a następnie użyć WaitAny, aby poczekać na to, które zakończy się jako pierwsze. Jeśli zadanie, które ukończyłeś jako pierwsze, jest Twoim zadaniem "roboczym", to gotowe. Jeśli zadanie, które zostało wykonane jako pierwsze, jest zadaniem timeout, wtedy możesz zareagować na ten limit czasu(np. żądanie anulowania).

 40
Author: Tomas Petricek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-07-19 01:15:38

A może 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ć zadania.Opcja Wait bez blokowania głównego wątku przy użyciu innego zadania.

 17
Author: as-cii,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-11-21 15:24:06

Oto w pełni opracowany 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 generyki zostały dodane, więc funkcja (lub zadanie) może zwrócić wartość. Oznacza to, że każda istniejąca funkcja może być zawinięta w funkcję timeout, 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();
            }
        }
    }
}

Caveats

Po podaniu tego odpowiedź, jej ogólnie Nie dobrą praktyką jest umieszczanie WYJĄTKÓW w kodzie podczas normalnej pracy, chyba że absolutnie musisz:

  • za każdym razem, gdy rzuca się wyjątek, jego niezwykle ciężka operacja,
  • wyjątki mogą spowolnić Kod o współczynnik 100 lub więcej, jeśli wyjątki są w ciasnej pętli.

Używaj tego kodu tylko wtedy, gdy absolutnie nie możesz zmienić funkcji, którą wywołujesz, aby wygasła po określonym TimeSpan.

Ta odpowiedź ma zastosowanie tylko w przypadku bibliotek innych firm, których po prostu nie można refaktorować, aby uwzględnić parametr timeout.

Jak napisać solidny kod

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

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

Jeśli Nie przestrzegasz tej zasady, Twój kod w końcu trafi w operację to z jakiegoś powodu nie powiedzie się, a następnie zablokuje się na czas nieokreślony, a Twoja aplikacja po prostu zawiesiła się na stałe.

Jeśli po pewnym czasie istniał rozsądny limit czasu, to aplikacja zawiesiłaby się na jakiś Ekstremalny czas (np. 30 sekund), a następnie wyświetlałaby błąd i kontynuowała swoją szczęśliwą drogę lub ponawiała próbę.

 12
Author: Contango,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-06-30 16:33:14

Użyj timera do obsługi wiadomości i automatycznego anulowania. Gdy zadanie się zakończy, zadzwoń do timerów tak, że nigdy nie będą strzelać. Poniżej znajduje się przykład; zmień taskDelay na 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);
                }
            }
        }
    }
}

Również, asynchroniczny CTP zapewnia TaskEx.Metoda opóźnienia, która zawija timery w zadania dla Ciebie. Może to dać ci większą kontrolę, aby zrobić rzeczy takie jak ustawić TaskScheduler dla kontynuacji, gdy timer zostanie wywołany.

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;
}
 8
Author: Quartermeister,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-11-21 15:40:50

Innym sposobem rozwiązania tego problemu jest użycie Reactive Extensions:

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 swoim teście jednostkowym, to 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żesz potrzebować następującej przestrzeni 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;
 6
Author: Kevan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-06-17 02:33:31

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

TimeSpan timeout = TimeSpan.FromSeconds(10);

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

TaskCanceledException zostanie wyrzucony w przypadku przekroczenia limitu czasu.

 5
Author: Cocowalla,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-25 10:05:28

Ogólna wersja powyższej odpowiedzi @ Kevan z rozszerzeniami Reactive.

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

Z opcjonalnym Schedulerem:

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

BTW: gdy wystąpi limit czasu, zostanie wyrzucony wyjątek limitu czasu

 2
Author: Jasper H Bojsen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-18 16:13:44

Czułem Task.Delay() zadanie i CancellationTokenSource w innych odpowiedziach trochę za dużo dla mojego przypadku użycia w ciasnej pętli sieciowej.

I chociaż Joe Hoag tworzy zadanie.Metoda TimeoutAfter na blogach MSDN była inspirująca, byłem trochę zmęczony używaniem TimeoutException do kontroli przepływu z tego samego powodu, co powyżej, ponieważ timeouty są oczekiwane częściej niż nie.

Więc poszedłem z tym, który również zajmuje się optymalizacjami wspomnianymi 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 jako takie:

var receivingTask = conn.ReceiveAsync(ct);

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

// Read and do something with data
var data = await receivingTask;
 0
Author: antak,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-09-14 03:00:41

Jeśli użyjesz BlockingCollection do zaplanowania zadania, producent może uruchomić potencjalnie długo działające zadanie, a konsument może użyć metody TryTake, która ma wbudowany Token timeout i cancellation.

 -1
Author: kns98,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-02-12 15:10:15