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

dtb*_*dtb 350 .net c# task-parallel-library

我想等待Task <T>完成一些特殊规则:如果在X毫秒后它还没有完成,我想向用户显示一条消息.如果它在Y毫秒后没有完成,我想自动请求取消.

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

And*_*ott 515

这个怎么样:

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

这里有一篇很棒的博客文章"Crafting a Task.TimeoutAfter Method"(来自MS Parallel Library团队),有关于此类事情的更多信息.

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

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
}
Run Code Online (Sandbox Code Playgroud)

  • 应该提到的是,即使Task.Delay可以在长时间运行任务之前完成,允许您处理超时场景,这也不会取消长时间运行的任务本身; WhenAny只是让你知道传递给它的任务之一已经完成.您必须实现CancellationToken并自行取消长时间运行的任务. (75认同)
  • 还可以注意到`Task.Delay`任务由系统计时器支持,系统计时器将继续被跟踪,直到超时到期,无论"SomeOperationAsync"需要多长时间.因此,如果这个整体代码片段在紧密循环中执行很多,那么您将为计时器消耗系统资源,直到它们都超时.解决这个问题的方法是将一个"CancellationToken"传递给`Task.Delay(timeout,cancellationToken)`,你在`SomeOperationAsync`完成时取消它来释放定时器资源. (24认同)
  • 取消代码正在做太多的工作.试试这个:int timeout = 1000; var cancellationTokenSource = new CancellationTokenSource(timeout); var cancellationToken = tokenSource.Token; var task = SomeOperationAsync(cancellationToken); 尝试{等待任务; //为此成功完成添加代码} catch(OperationCancelledException){//此处为超时案例添加代码} (6认同)
  • @TomexOu:问题是如何“异步”等待任务完成。`Task.Wait(timeout)` 将同步阻塞而不是异步等待。 (4认同)
  • @ilans 通过等待`Task`,任务存储的任何异常都会在此时重新抛出。这使您有机会捕获“OperationCanceledException”(如果取消)或任何其他异常(如果出现故障)。 (3认同)
  • 值得注意的是,这个解决方案(虽然很棒)需要.Net 4.5或更高版本......我们这些使用.Net 4.0的人都是SOL. (2认同)

Law*_*ton 189

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

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.");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 原始任务(`task`)是否会在超时时继续运行? (7认同)
  • 给这个人一些票.优雅的解决方 如果你的电话没有返回类型,请确保你删除了TResult. (6认同)
  • CancellationTokenSource是一次性的,应该在"using"块中 (5认同)
  • @Itatrap等待任务两次只是在第二次等待时返回结果.它不会执行两次.你可以说它在执行两次时等于`task.Result`. (4认同)
  • @jag,是的,原来的任务将继续运行。下面还有其他答案(包括我的),它们在超时时取消原始任务。 (4认同)
  • 较小的改进机会:`TimeoutException`具有适当的默认消息。用“操作已超时”覆盖它。没有添加任何值,实际上暗示有理由重写它,从而引起一些混乱。 (3认同)
  • @Kir 不,它没有,实际上将 await 与 Task.WhenAny 一起使用是允许它*不* 阻止的原因(控制权返回给调用者,直到等待的任务完成)。也许您正在考虑使用 Task.WaitAny 的 Tomas Petricek 的解决方案 http://stackoverflow.com/a/4238575/1512 之类的东西,它会阻塞直到其中一项任务完成。 (2认同)
  • 为了跟进我的上一条评论,我刚刚测试了这个,取消的 Task.Delay 不会引发 `TaskScheduler.UnobservedTaskException` 事件 (2认同)

Vij*_*mal 102

从 .Net 6(预览版 7)或更高版本开始,有一个新的内置方法Task.WaitAsync可以实现此目的。

// Using TimeSpan
await myTask.WaitAsync(TimeSpan.FromSeconds(10));

// Using CancellationToken
await myTask.WaitAsync(cancellationToken);

// Using both TimeSpan and CancellationToken
await myTask.WaitAsync(TimeSpan.FromSeconds(10), cancellationToken);
Run Code Online (Sandbox Code Playgroud)

如果任务在TimeSpan或之前没有完成,则分别CancellationToken抛出TimeoutExceptionTaskCanceledException

try
{
    await myTask.WaitAsync(TimeSpan.FromSeconds(10), cancellationToken);

}
catch (TaskCanceledException)
{
    Console.WriteLine("Task didn't get finished before the `CancellationToken`");
}
catch (TimeoutException)
{
    Console.WriteLine("Task didn't get finished before the `TimeSpan`");
}
Run Code Online (Sandbox Code Playgroud)


Tom*_*cek 47

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

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

  • 一个线程将被阻止 - 但如果你好,那么没问题.我采取的解决方案是下面的解决方案,因为没有线程被阻止.我看了一篇非常好的博文. (3认同)

Jos*_*áha 24

这是先前答案的略微增强版本。

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法

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;
}
Run Code Online (Sandbox Code Playgroud)


as-*_*cii 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();
    }
Run Code Online (Sandbox Code Playgroud)

您可以使用Task.Wait选项而不使用另一个任务阻止主线程.


Con*_*ngo 14

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

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

此答案中实现的主要优点是已添加泛型,因此函数(或任务)可以返回值.这意味着任何现有函数都可以包含在超时函数中,例如:

之前:

int x = MyFunc();
Run Code Online (Sandbox Code Playgroud)

后:

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

此代码需要.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();
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

注意事项

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

  • 每次抛出异常,它都是一个非常重量级的操作,
  • 如果异常处于紧密循环中,则异常会使代码减慢100倍或更多.

如果你绝对不能改变你正在调用的函数,那么只能使用这段代码,因此它会在特定时间后超时TimeSpan.

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

如何编写健壮的代码

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

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

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

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


Qua*_*ter 8

使用计时器处理消息和自动取消.任务完成后,在计时器上调用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);
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

此外,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;
}
Run Code Online (Sandbox Code Playgroud)

  • @dtb:如果你将t1设为Task <Task <Result >>然后调用TaskExtensions.Unwrap怎么办?你可以从你的内部lambda返回t2,然后你可以在unwrapped任务中添加continuation. (2认同)

Arm*_*and 8

使用 .Net 6(预览版 7 作为此答案的日期),可以使用新的WaitAsync(TimeSpan, CancellationToken)来满足此特定需求。如果您可以使用 .Net6,如果我们与本文中提出的大多数好的解决方案进行比较,则该版本还被描述为优化的。

(感谢所有参与者,因为我使用你们的解决方案很多年了)


Coc*_*lla 7

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

TimeSpan timeout = TimeSpan.FromSeconds(10);

using (var cts = new CancellationTokenSource(timeout))
{
    await myTask.WaitAsync(cts.Token);
}
Run Code Online (Sandbox Code Playgroud)

TaskCanceledException 将在超时的情况下抛出.

  • 现在它已内置到 .Net 6 中!https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.waitasync?view=net-6.0 (4认同)

小智 6

解决此问题的另一种方法是使用Reactive Extensions:

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

在单元测试中使用以下代码进行上述测试,它适用于我

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);
Run Code Online (Sandbox Code Playgroud)

您可能需要以下命名空间:

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;
Run Code Online (Sandbox Code Playgroud)


Jas*_*sen 5

上面@Kevan 的回答的通用版本,使用 Reactive Extensions。

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

使用可选的调度程序:

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();
}
Run Code Online (Sandbox Code Playgroud)

BTW:当超时发生时,会抛出超时异常


flo*_*dis 5

为了好玩,我为任务做了一个“OnTimeout”扩展。超时时,Task 执行所需的内联 lambda Action() 并返回 true,否则返回 false。

public static async Task<bool> OnTimeout<T>(this T t, Action<T> action, int waitms) where T : Task
{
    if (!(await Task.WhenAny(t, Task.Delay(waitms)) == t))
    {
        action(t);
        return true;
    } else {
        return false;
    }
}
Run Code Online (Sandbox Code Playgroud)

OnTimeout 扩展返回一个 bool 结果,可以将其分配给变量,如调用 UDP 套接字异步的示例所示:

var t = UdpSocket.ReceiveAsync();

var timeout = await t.OnTimeout(task => {
    Console.WriteLine("No Response");
}, 5000);
Run Code Online (Sandbox Code Playgroud)

可以在超时 lambda 中访问“任务”变量以进行更多处理。

使用接收对象的 Action 可能会启发各种其他扩展设计。