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

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


当前回答

如果使用BlockingCollection来调度任务,生产者可以运行可能长时间运行的任务,消费者可以使用TryTake方法,该方法具有内置的超时和取消令牌。

其他回答

这是对之前答案的稍微强化版。

除了Lawrence的答案之外,它还会在超时发生时取消原来的任务。 除了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;
}

这个怎么样:

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

这里有一篇很棒的博客文章“制定任务”。TimeoutAfter Method”(来自MS并行库团队)提供了关于这类事情的更多信息。

补充:在我的回答的评论请求,这里是一个扩展的解决方案,包括取消处理。请注意,将取消传递给任务和计时器意味着在代码中可以经历多种取消方式,您应该确保测试并确信您正确地处理了所有这些方法。不要让各种组合的机会,并希望您的计算机在运行时做正确的事情。

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完成时,对计时器调用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;
}

安德鲁·阿诺特(Andrew Arnott)回答的几个变体:

If you want to wait for an existing task and find out whether it completed or timed out, but don't want to cancel it if the timeout occurs: 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 } } If you want to start a work task and cancel the work if the timeout occurs: 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 } If you have a task already created that you want to cancel if a timeout occurs: 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

创建一个扩展来等待任务或延迟完成,以先发生者为准。如果延迟成功,则抛出异常。

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