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

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


当前回答

所以这是古老的,但有一个更好的现代解决方案。不确定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调用中。运行,传递超时令牌。

这假设您正在传递一个取消令牌(令牌变量)。如果不需要在超时后单独取消任务,则可以直接使用timeoutTokenSource。否则,您将创建linkedTokenSource,它将在超时发生或以其他方式取消时取消。

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

此外,我在这里使用的是c# 7中引入的局部函数,但您可以很容易地使用lambda或实际函数来达到同样的效果。类似地,c# 8为使用语句引入了更简单的语法,但这些语法很容易重写。

其他回答

为了它的乐趣,我做了一个“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接收对象的使用可能会启发其他各种扩展设计。

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

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

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

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

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

所以这是古老的,但有一个更好的现代解决方案。不确定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调用中。运行,传递超时令牌。

这假设您正在传递一个取消令牌(令牌变量)。如果不需要在超时后单独取消任务,则可以直接使用timeoutTokenSource。否则,您将创建linkedTokenSource,它将在超时发生或以其他方式取消时取消。

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

此外,我在这里使用的是c# 7中引入的局部函数,但您可以很容易地使用lambda或实际函数来达到同样的效果。类似地,c# 8为使用语句引入了更简单的语法,但这些语法很容易重写。