Asynchronously wait for Task to complete with timeout

后端 未结 16 2020
天命终不由人
天命终不由人 2020-11-21 17:49

I want to wait for a Task to complete with some special rules: If it hasn\'t completed after X milliseconds, I want to display a message to the user. And

相关标签:
16条回答
  • 2020-11-21 18:19

    Another way of solving this problem is using Reactive Extensions:

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

    Test up above using below code in your unit test, it works for me

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

    You may need the following namespace:

    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;
    
    0 讨论(0)
  • 2020-11-21 18:20

    Using Stephen Cleary's excellent AsyncEx library, you can do:

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

    TaskCanceledException will be thrown in the event of a timeout.

    0 讨论(0)
  • 2020-11-21 18:21

    What about something like this?

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

    You can use the Task.Wait option without blocking main thread using another Task.

    0 讨论(0)
  • 2020-11-21 18:24

    How about this:

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

    And here's a great blog post "Crafting a Task.TimeoutAfter Method" (from MS Parallel Library team) with more info on this sort of thing.

    Addition: at the request of a comment on my answer, here is an expanded solution that includes cancellation handling. Note that passing cancellation to the task and the timer means that there are multiple ways cancellation can be experienced in your code, and you should be sure to test for and be confident you properly handle all of them. Don't leave to chance various combinations and hope your computer does the right thing at runtime.

    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
    }
    
    0 讨论(0)
  • 2020-11-21 18:24

    So this is ancient, but there's a much better modern solution. Not sure what version of c#/.NET is required, but this is how I do it:

    
    ... Other method code not relevant to the question.
    
    // a token source that will timeout at the specified interval, or if cancelled outside of this scope
    using var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5));
    using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, timeoutTokenSource.Token);
    
    async Task<MessageResource> FetchAsync()
    {
        try
        {
            return await MessageResource.FetchAsync(m.Sid);
        } catch (TaskCanceledException e)
        {
            if (timeoutTokenSource.IsCancellationRequested)
                throw new TimeoutException("Timeout", e);
            throw;
        }
    }
    
    return await Task.Run(FetchAsync, linkedTokenSource.Token);
    

    the CancellationTokenSource constructor takes a TimeSpan parameter which will cause that token to cancel after that interval has elapsed. You can then wrap your async (or syncronous, for that matter) code in another call to Task.Run, passing the timeout token.

    This assumes you're passing in a cancellation token (the token variable). If you don't have a need to cancel the task separately from the timeout, you can just use timeoutTokenSource directly. Otherwise, you create linkedTokenSource, which will cancel if the timeout ocurrs, or if it's otherwise cancelled.

    We then just catch OperationCancelledException and check which token threw the exception, and throw a TimeoutException if a timeout caused this to raise. Otherwise, we rethrow.

    Also, I'm using local functions here, which were introduced in C# 7, but you could easily use lambda or actual functions to the same affect. Similarly, c# 8 introduced a simpler syntax for using statements, but those are easy enough to rewrite.

    0 讨论(0)
  • 2020-11-21 18:29

    A generic version of @Kevan's answer above, using Reactive Extensions.

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

    With optional Scheduler:

    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: When a Timeout happens, a timeout exception will be thrown

    0 讨论(0)
提交回复
热议问题