如何为不接受取消令牌的异步函数设置超时?
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;
【讨论】:
这使得它是同步的,这并不理想。 只有读取的部分。它是否可以接受取决于更重要的事情 - 能够超时或保持异步而不控制进程。以上是关于如何为不接受取消令牌的异步函数设置超时?的主要内容,如果未能解决你的问题,请参考以下文章
如何为默认的swagger android客户端设置连接和套接字超时