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

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


当前回答

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

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

其他回答

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

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

像这样的东西怎么样?

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

您可以使用任务。等待选项,不阻塞主线程使用另一个任务。

我将这里的一些其他答案和另一个线程上的这个答案重新组合成一个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);
    }
}    

安德鲁·阿诺特(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

为了它的乐趣,我做了一个“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;
    }
}

OnTimeout扩展返回一个bool结果,可以分配给一个变量,就像这个例子调用UDP套接字Async:

var t = UdpSocket.ReceiveAsync();

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

在timeout lambda中可以访问“task”变量以进行更多处理。

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