首页 文章

异步等待Task <T>以超时完成

提问于
浏览
293

我想等一个Task<T>完成一些特殊规则:如果它在Y毫秒后没有完成't completed after X milliseconds, I want to display a message to the user. And if it hasn't,我想自动request cancellation .

我可以使用Task.ContinueWith来异步等待任务完成(即,在任务完成时安排执行操作),但这不允许指定超时 . 我可以使用Task.Wait同步等待任务完成超时,但是阻止了我的线程 . 如何异步等待任务完成超时?

12 回答

  • 13

    您可以使用 Task.WaitAny 等待多个任务中的第一个 .

    您可以创建两个额外的任务(在指定的超时后完成),然后使用 WaitAny 等待先完成的任何一个 . 如果首先完成的任务是您的"work"任务,那么您就完成了 . 如果首先完成的任务是超时任务,那么您可以对超时做出反应(例如请求取消) .

  • 42

    这个怎么样:

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

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

    Addition :根据对我的回答发表评论的请求,这是一个扩展的解决方案,其中包括取消处理 . 请注意,将取消传递给任务和计时器意味着可以通过多种方式在您的代码中体验取消,并且您应该确保测试并确信您正确处理了所有这些 . 不要忘记各种组合,并希望您的计算机在运行时做正确的事情 .

    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
    }
    
  • -1

    这是一个扩展方法版本,它结合了原始任务完成时取消超时,正如Andrew Arnott在his answer的评论中所建议的那样 .

    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.");
            }
        }
    }
    
  • 141

    这样的事情怎么样?

    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选项而不使用另一个任务阻止主线程 .

  • 6

    这是一个基于最高投票答案的完整工作示例,它是:

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

    Caveats

    给出了这个答案之后,在正常操作期间在代码中抛出异常通常不是一个好习惯,除非你绝对必须:

    • 每次抛出异常时,它都是极其重量级的操作,

    • 如果异常处于紧密循环中,异常会使代码减慢100倍或更多 .

    如果您绝对无法更改正在调用的函数,请仅使用此代码,以便在特定 TimeSpan 之后超时 .

    这个答案实际上只适用于处理您无法重构以包含超时参数的第三方库库 .

    How to write robust code

    如果你想编写健壮的代码,一般规则如下:

    每个可能无限期阻止的操作都必须超时 .

    如果您没有遵守此规则,您的代码最终会遇到因某些原因失败的操作,然后它将无限期地阻止,并且您的应用程序刚刚永久挂起 .

    如果在一段时间后有一个合理的超时,那么你的应用程序会挂起一段极端的时间(例如30秒)然后它会显示错误并继续其快乐方式,或重试 .

  • 6

    使用Timer处理消息并自动取消 . 任务完成后,在计时器上调用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方法,该方法将为您分配任务中的计时器 . 这可以为您提供更多控制,例如在Timer触发时设置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;
    }
    
  • -1

    解决此问题的另一种方法是使用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;
    
  • 8

    使用Stephen Cleary优秀的AsyncEx库,您可以:

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

    如果超时,将抛出 TaskCanceledException .

  • 2

    @ Kevan在上面使用Reactive Extensions回答的通用版本 .

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

    使用可选的Scheduler:

    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:当超时发生时,超时异常将是抛出

  • 17

    Andrew Arnott的几个变种答案:

    • 如果要等待现有任务并确定其是否已完成或超时,但如果发生超时则不想取消它:
    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
        }
    }
    
    • 如果要启动工作任务并在超时发生时取消工作:
    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
    }
    
    • 如果您已经创建了一个要在超时发生时取消的任务:
    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

  • 441

    如果使用BlockingCollection来计划任务,则 生产环境 者可以运行可能长时间运行的任务,并且使用者可以使用内置超时和取消令牌的TryTake方法 .

  • 0

    我感觉 Task.Delay() 任务和 CancellationTokenSource 在另一个答案中对我的用例在一个紧凑的网络循环中有点多 .

    虽然Joe Hoag's Crafting a Task.TimeoutAfter Method on MSDN blogs鼓舞人心,但由于与上述相同的原因,我对使用 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;
    

相关问题