ChatGPT解决这个技术问题 Extra ChatGPT

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

我想通过一些特殊规则等待 Task<T> 完成:如果 X 毫秒后仍未完成,我想向用户显示一条消息。如果 Y 毫秒后还没有完成,我想自动request cancellation

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

你说的对。我很惊讶它没有提供超时。也许在 .NET 5.0 中......当然我们可以在任务本身中构建超时,但这并不好,这些东西必须免费。
虽然您描述的两层超时仍然需要逻辑,但 .NET 4.5 确实提供了一种简单的方法来创建基于超时的 CancellationTokenSource。构造函数有两种重载,一种采用整数毫秒延迟,另一种采用 TimeSpan 延迟。
完整的简单库源代码在这里:stackoverflow.com/questions/11831844/…
任何具有完整源代码工作的最终解决方案?可能更复杂的示例用于通知每个线程中的错误,并且在 WaitAll 显示摘要之后?
要添加到 @patridge 建议的内容,也可以使用 CancellationTokenSource.CancelAfter(<timespan or millisecs>) 来实现

A
Andrew Arnott

这个怎么样:

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

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

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
}

需要说明的是,即使 Task.Delay 可以在长时间运行的任务之前完成,允许您处理超时情况,这并不会取消长时间运行的任务本身; WhenAny 只是让您知道传递给它的任务之一已经完成。您必须自己实现一个 CancellationToken 并取消长时间运行的任务。
还可以注意到,Task.Delay 任务由系统计时器支持,无论 SomeOperationAsync 花费多长时间,系统计时器都将继续跟踪该计时器,直到超时到期。因此,如果整个代码片段在一个紧密的循环中执行了很多,那么您正在为计时器消耗系统资源,直到它们全部超时。解决此问题的方法是将 CancellationToken 传递给 Task.Delay(timeout, cancellationToken),当 SomeOperationAsync 完成以释放计时器资源时取消它。
取消代码做了太多的工作。试试这个: int timeout = 1000; var cancelTokenSource = new CancellationTokenSource(timeout); var cancelToken = tokenSource.Token; var task = SomeOperationAsync(cancellationToken);尝试{等待任务; // 此处添加代码成功完成 } catch (OperationCancelledException) { // 此处添加代码超时情况 }
@ilans 通过等待 Task,任务存储的任何异常都会在此时重新抛出。这使您有机会捕获 OperationCanceledException(如果已取消)或任何其他异常(如果出现故障)。
@TomexOu:问题是如何异步等待任务的完成。 Task.Wait(timeout) 将同步阻塞而不是异步等待。
L
Lawrence Johnston

这是一个扩展方法版本,其中包含在原始任务完成时取消超时,正如 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.");
        }
    }
}

给这个人一些选票。优雅的解决方案。如果您的呼叫没有返回类型,请确保您只删除 TResult。
CancellationTokenSource 是一次性的,应该在 using 块中
@It'satrap 两次等待任务只是在第二次等待时返回结果。它不会执行两次。您可以说它在执行两次时等于 task.Result
如果发生超时,原始任务 (task) 是否仍会继续运行?
次要改进机会:TimeoutException 具有合适的默认消息。用“操作已超时”覆盖它。没有增加任何价值,实际上通过暗示有理由覆盖它而引起一些混乱。
r
ruffin

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

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


我见过我非常尊重的 MVP 使用的这种技术,对我来说它似乎比公认的答案更干净。也许一个例子将有助于获得更多选票!我会自愿这样做,除非我没有足够的任务经验来确信它会有所帮助:)
一个线程会被阻塞 - 但如果你同意,那就没问题了。我采取的解决方案是下面的解决方案,因为没有线程被阻塞。我读了一篇非常好的博客文章。
@JJschk 您提到您采用了解决方案 below .... 那是什么?基于 SO 排序?
如果我不想取消较慢的任务怎么办?我想在它完成但从当前方法返回时处理它..
T
Theodor Zoulias

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

除了劳伦斯的回答,它在超时发生时取消了原来的任务。

除了 sjb 的答案变体 2 和 3,您可以为原始任务提供 CancellationToken,当发生超时时,您会得到 TimeoutException 而不是 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();
        }
    }
}

用法

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

感谢您的解决方案!似乎您应该将 timeoutCancellation 传递给 delayTask。目前,如果您取消,CancelAfterAsync 可能会抛出 TimeoutException 而不是 TaskCanceledException,因为 delayTask 可能会先完成。
@AxelUser,你是对的。我花了一个小时进行大量单元测试来了解发生了什么:) 我假设当给 WhenAny 的两个任务都被相同的令牌取消时,WhenAny 将返回第一个任务。这个假设是错误的。我已经编辑了答案。谢谢!
我很难弄清楚如何使用定义的 Task 函数实际调用它;你有没有机会举例说明如何调用它?
@jhaagsma,添加了示例!
@JosefBláha 非常感谢!我仍然在慢慢地围绕 lambda 样式语法,这对我来说是没有想到的 - 通过传入 lambda 函数,令牌被传递到 CancelAfterAsync 的主体中的任务。漂亮!
V
Vijay Nirmal

从 .Net 6(Preview 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);

C
Cocowalla

使用 Stephen Cleary 出色的 AsyncEx 库,您可以:

TimeSpan timeout = TimeSpan.FromSeconds(10);

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

TaskCanceledException 将在超时的情况下被抛出。


现在已内置到 .Net 6 中! docs.microsoft.com/en-us/dotnet/api/…
a
as-cii

这样的事情呢?

    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 选项,而无需使用另一个任务阻塞主线程。


事实上,在这个例子中,你不是在 t1 内部等待,而是在一个上层任务上等待。我会尝试做一个更详细的例子。
C
Contango

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

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

注意事项

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

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

如果异常处于紧密循环中,异常可能会使您的代码速度降低 100 倍或更多。

仅当您绝对无法更改您正在调用的函数以使其在特定 TimeSpan 后超时时才使用此代码。

此答案仅在处理您根本无法重构以包含超时参数的第 3 方库时才适用。

如何编写健壮的代码

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

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

如果你不遵守这条规则,你的代码最终会遇到一个由于某种原因而失败的操作,然后它会无限期地阻塞,你的应用程序就会永久挂起。

如果一段时间后出现合理的超时,那么您的应用程序将挂起一段极端的时间(例如 30 秒),然后它会显示错误并继续以愉快的方式继续,或者重试。


Q
Quartermeister

使用 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 方法,它将为您将计时器包装在任务中。这可以让您更好地控制执行诸如设置 TaskScheduler 以在 Timer 触发时继续执行之类的操作。

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

他不希望当前线程被阻塞,即没有task.Wait()
@Danny:那只是为了使示例完整。在 ContinueWith 之后,您可以返回并让任务运行。我将更新我的答案以使其更清楚。
@dtb:如果您将 t1 设为 Task> 然后调用 TaskExtensions.Unwrap 会怎样?您可以从您的内部 lambda 返回 t2,然后您可以将延续添加到展开的任务中。
惊人的!这完美地解决了我的问题。谢谢!我想我会接受@AS-CII 提出的解决方案,尽管我希望我也能接受你的回答,因为我建议 TaskExtensions.Unwrap 我应该打开一个新问题,这样你就可以获得你应得的代表吗?
A
Armand

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

(感谢所有参与者,因为我多年来一直使用您的解决方案)


K
Kevan

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

1
1iveowl

上面@Kevan 答案的通用版本,使用响应式扩展。

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

使用可选的调度程序:

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


f
flodis

为了好玩,我对 Task 做了一个“OnTimeout”扩展。超时任务执行所需的内联 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;
    }
}

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

var t = UdpSocket.ReceiveAsync();

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

'task' 变量可在超时 lambda 中访问以进行更多处理。

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


a
antak

我觉得 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;

s
sjb-sjb

Andrew Arnott 回答的一些变体:

如果您想等待现有任务并了解它是否已完成或超时,但如果发生超时又不想取消它: public static async Task TimedOutAsync(this Task task, int timeoutMilliseconds) { if (timeoutMilliseconds < 0 || (timeoutMilliseconds > 0 && timeoutMilliseconds < 100)) { throw new ArgumentOutOfRangeException(); } if (timeoutMilliseconds == 0) { return !task.IsCompleted; // 如果未完成则超时 } var cts = new CancellationTokenSource(); if (await Task.WhenAny(task, Task.Delay(timeoutMilliseconds, cts.Token)) == task) { cts.Cancel(); // 任务完成,摆脱定时器等待任务; // 测试异常或任务取消 return false; // 没有超时 } else { return true; // 是否超时 } } 如果要启动一个工作任务,如果发生超时则取消工作: public static async Task CancelAfterAsync( this Func> actionAsync, int timeoutMilliseconds) { if (timeoutMilliseconds < 0 || (timeoutMilliseconds > 0 && timeoutMilliseconds < 100)) { throw new ArgumentOutOfRangeException(); } var taskCts = new CancellationTokenSource(); var timerCts = new CancellationTokenSource();任务 任务 = actionAsync(taskCts.Token); if (await Task.WhenAny(task, Task.Delay(timeoutMilliseconds, timerCts.Token)) == task) { timerCts.Cancel(); // 任务完成,去掉定时器 } else { taskCts.Cancel(); // 定时器完成,摆脱任务 } return await task; // 测试异常或任务取消 } 如果你已经创建了一个任务,如果发生超时你想取消: public static async Task CancelAfterAsync(this Task 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(); // 任务完成,去掉定时器 } else { taskCts.Cancel(); // 定时器完成,摆脱任务 } return await task; // 测试异常或任务取消 }

另外说明一下,这些版本如果没有超时就会取消定时器,所以多次调用不会导致定时器堆积。

sjb


t
thomasrea0113

所以这是古老的,但有一个更好的现代解决方案。不确定需要什么版本的 c#/.NET,但我就是这样做的:


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

CancellationTokenSource 构造函数采用 TimeSpan 参数,该参数将导致该令牌在该间隔过去后取消。然后,您可以将异步(或同步)代码包装在对 Task.Run 的另一个调用中,并传递超时令牌。

这假设您正在传递取消令牌(token 变量)。如果不需要和超时分开取消任务,直接使用timeoutTokenSource即可。否则,您创建 linkedTokenSource,如果发生超时,它将取消, 如果它被取消。

然后我们只捕获 OperationCancelledException 并检查哪个令牌引发了异常,如果超时导致引发此异常,则抛出 TimeoutException。否则,我们重新抛出。

另外,我在这里使用了本地函数,这是在 C# 7 中引入的,但是您可以轻松地使用 lambda 或实际函数来实现相同的效果。同样,c# 8 引入了一种更简单的 using 语句语法,但这些语法很容易重写。


E
Edward Brey

创建扩展以等待任务或延迟完成,以先到者为准。如果延迟获胜,则抛出异常。

public static async Task<TResult> WithTimeout<TResult>(this Task<TResult> task, TimeSpan timeout)
{
    if (await Task.WhenAny(task, Task.Delay(timeout)) != task)
        throw new TimeoutException();
    return await task;
}

k
kns98

如果您使用 BlockingCollection 来安排任务,生产者可以运行可能长时间运行的任务,而消费者可以使用内置超时和取消令牌的 TryTake 方法。


我必须写一些东西(不想在这里放专有代码),但场景是这样的。生产者将是执行可能超时的方法的代码,并在完成后将结果放入队列中。消费者将在超时时调用 trytake() 并在超时时接收令牌。生产者和消费者都将是后台任务,并在需要时使用 UI 线程调度程序向用户显示消息。
t
tm1

我在这里将其他一些答案的想法和 this answer on another thread 重新组合成 Try 样式的扩展方法。如果您想要一个扩展方法,这有一个好处,但在超时时避免异常。

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

T
Theodor Zoulias

这是一个 WaitAsync 方法的低级实现,它同时接受超时和 CancellationToken,如果发生异常,则会传播 Task<T> 的所有错误,而不仅仅是第一个错误:

public static Task<TResult> WaitAsync<TResult>(this Task<TResult> task,
    TimeSpan timeout, CancellationToken cancellationToken = default)
{
    if (task == null) throw new ArgumentNullException(nameof(task));
    if (timeout < TimeSpan.Zero && timeout != Timeout.InfiniteTimeSpan)
        throw new ArgumentOutOfRangeException(nameof(timeout));

    var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
    cts.CancelAfter(timeout);

    return task
        .ContinueWith(_ => { }, cts.Token,
            TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default)
        .ContinueWith(continuation =>
        {
            cts.Dispose();
            if (task.IsCompleted) return task;
            cancellationToken.ThrowIfCancellationRequested();
            if (continuation.IsCanceled) throw new TimeoutException();
            return task;
        }, TaskScheduler.Default).Unwrap();
}

如果在 task 完成之前超时已过,则会引发 TimeoutException

在这种情况下,诚实地传播所有错误并不是真正的增值功能。原因是,如果您像这样使用 WaitAsyncawait someTask.WaitAsync(timeout),任何额外的错误都将被 await 运算符吞下,按照设计,它只会传播等待任务的第一个异常。将 WaitAsync 任务存储在变量中并在 catch 块中检查它没有多大意义,因为您已经有 someTask 可用,您可以改为检查它。