如何为不接受取消令牌的异步函数设置超时?

Posted

技术标签:

【中文标题】如何为不接受取消令牌的异步函数设置超时?【英文标题】:How can I set a timeout for an Async function that doesn't accept a cancellation token? 【发布时间】:2014-09-23 01:18:58 【问题描述】:

我的网络请求由这段代码处理;

Response = await Client.SendAsync(Message, HttpCompletionOption.ResponseHeadersRead, CToken);

在读取响应标头之后和内容读取完成之前返回。当我调用这条线来获取内容时......

return await Response.Content.ReadAsStringAsync();

我希望能够在 X 秒后停止它。但它不接受取消令牌。

【问题讨论】:

仅供参考,如果您正在处理 IDisposable 的超时问题,您可能想看看这个:Async network operations never finish 这能回答你的问题吗? Asynchronously wait for Task<T> to complete with timeout 【参考方案1】:

虽然您可以依赖WithCancellation 进行重用,但更简单的超时解决方案(不会抛出OperationCanceledException)是使用Task.Delay 和使用Task.WhenAny等待第一个任务完成:

public static Task<TResult> WithTimeout<TResult>(this Task<TResult> task, TimeSpan timeout)

    var timeoutTask = Task.Delay(timeout).ContinueWith(_ => default(TResult), TaskContinuationOptions.ExecuteSynchronously);
    return Task.WhenAny(task, timeoutTask).Unwrap();

或者,如果您想在超时时抛出异常,而不是仅仅返回默认值(即null):

public static async Task<TResult> WithTimeout<TResult>(this Task<TResult> task, TimeSpan timeout)

    if (task == await Task.WhenAny(task, Task.Delay(timeout)))
    
        return await task;
    
    throw new TimeoutException();

而用法是:

var content = await Response.Content.ReadAsStringAsync().WithTimeout(TimeSpan.FromSeconds(1));

【讨论】:

虽然 NedStoyanov 的回答也很棒,但它专门提供了处理超时的代码,而不仅仅是允许取消。我觉得这个更好地回答了这个问题。 但是能保证Delay任务不会排在任务之前吗?任务可能没有问题,但任务调度程序仍然可以先运行延迟(如果所有任务容量都已满,它必须选择)? @DavidS。 Task.Delay 并没有真正安排好,因为它并没有真正“运行”。它只是启动一个计时器,该计时器在超时后弹出并完成任务。此外,您获得的任务不需要“开始”。它已经很热了,所以这里没有真正的担心(除非在非常极端的情况下)。【参考方案2】:

看看How do I cancel non-cancelable async operations?。如果您只是希望await 在后台继续请求时完成,您可以使用作者的WithCancellation 扩展方法。此处转载自文章:

public static async Task<T> WithCancellation<T>( 
    this Task<T> task, CancellationToken cancellationToken) 
 
    var tcs = new TaskCompletionSource<bool>(); 
    using(cancellationToken.Register( 
                s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs)) 
        if (task != await Task.WhenAny(task, tcs.Task)) 
            throw new OperationCanceledException(cancellationToken); 
    return await task; 

它本质上将原始任务与接受取消令牌的任务结合起来,然后使用Task.WhenAny 等待这两个任务。因此,当您取消 CancellationToken 时,secodn 任务会被取消,但原来的任务会继续进行。只要你不关心,你可以使用这个方法。

你可以这样使用它:

return await Response.Content.ReadAsStringAsync().WithCancellation(token);

更新

您也可以尝试将响应作为取消的一部分进行处理。

token.Register(Reponse.Content.Dispose);
return await Response.Content.ReadAsStringAsync().WithCancellation(token);

现在,当您取消令牌时,Content 对象将被释放。

【讨论】:

非常感谢,那么没有办法停止正在进行的 ReadAsStringAsync 吗?这是一个很好的解决方案,但理想情况下,如果可能的话,我想中止 ReadAsStringAsync。 你可以使用Thread.Abort,就像这个答案***.com/a/21715122/1239433一样,但你可能必须在Task.Run中调用函数的同步版本才能做到这一点。 @iguanaman:传统上,如果底层句柄关闭,Microsoft API 将取消其 I/O 操作。因此,您可以尝试在发出取消令牌信号时处理 Response.Content 和/或 Response【参考方案3】:

由于返回一个任务,你可以Wait为这个任务,本质上相当于指定一个超时:

// grab the task object
var reader = response.Content.ReadAsStringAsync();

// so you're telling the reader to finish in X milliseconds
var timedOut = reader.Wait(X);  

if (timedOut)

    // handle timeouts

else

    return reader.Result;

【讨论】:

这使得它是同步的,这并不理想。 只有读取的部分。它是否可以接受取决于更重要的事情 - 能够超时或保持异步而不控制进程。

以上是关于如何为不接受取消令牌的异步函数设置超时?的主要内容,如果未能解决你的问题,请参考以下文章

使用取消令牌为用户定义的函数设置超时

如何为 MFC 线程设置超时

如何为默认的swagger android客户端设置连接和套接字超时

如何为 AndroidAsync websockets 设置超时?

如何为加载已关闭的外部 javascript 文件设置超时

Polly Timeout 乐观使用 HttpClientFactory。如何使用取消令牌?