使用Task.WhenAny来处理超时,如何获取任务结果?

6

我需要在一个移动应用程序的任务调用中添加超时功能。我尝试使用Task.WhenAny完成这个功能,如下所示。该方法返回最先完成的任务。我的问题是,如果任务没有超时,我仍然需要从这个任务中获取返回值。

Task task = restService.GetRequestAsync(articleAdr, articleParams);
var response = await Task.WhenAny(task, Task.Delay(1000, cts.Token));

响应只是首先完成的任务。我怎样才能得到它的结果?

4个回答

3
我可以想到三种不同的可能性来解决这个场景。
前两种可以在Peter Bons' answer中找到。
第三种是先存储你的两个任务,然后在完成await Task.WhenAny()后检查状态。
var workerTask = restService.GetRequestAsync(articleAdr, articleParams);
var cancellationTask = Task.Delay(1000, cts.Token);

await Task.WhenAny(workerTask, cancellationTask);
if (workerTask.Status == TaskStatus.RanToCompletion)
{
  // Note that this is NOT a blocking call because the Task ran to completion.
  var response = workerTask.Result;

  // Do whatever work with the completed result.
}
else
{
  // Handle the cancellation.
  // NOTE: You do NOT want to call workerTask.Result here. It will be a blocking call and will block until 
  // your previous method completes, especially since you aren't passing the CancellationToken.
}

1
我认为你可以看一下@jamesmontemagno的MVVM Helpers。有一个扩展可以帮助你为任务添加超时时间。 MVVM Helpers - Utils 在这里,你可以找到一个视频,James解释了如何使用它。 The-Xamarin-Show-12-MVVM-Helpers (约26:38分钟)

1

然而有一个问题,你的 CancellationTokenSource 是如何创建和初始化的?什么时候调用 Cancel 方法?

如果可能的话,最好是你的方法 GetRequestAsync 接受一个 CancellationToken。因为你可以创建一个 CancellationTokenSource 在一定时间后发出取消请求,这样就可以避免调用 Task.WhenAny 了。

总的来说,有多种选项,下面介绍其中一种:

// Set timeout of 1 second
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(1)); 

...

Task task = restService.GetRequestAsync(articleAdr, articleParams);

// Wait until timeout or request is done.
await Task.WhenAny(task, Task.Delay(TimeSpan.FromMilliseconds(-1), cts.Token));

// If the cancellation is not yet requested the request was done before the timeout was reached
if(!cts.IsCancellationRequested)
{
    var response = await task;  
} 

另一个选择是这个:


Task requestTask = restService.GetRequestAsync(articleAdr, articleParams);
var firstCompletedTask = await Task.WhenAny(requestTask, Task.Delay(1000, cts.Token));
if(firstCompletedTask == requestTask)
{
    cts.Cancel(); // optionally, will cancel the delay task since it is of no use any more.
    var response = await requestTask;
}

已完成的任务可以被等待多次,每次都会产生相同的结果。


1
我需要在移动应用程序中的任务调用中添加超时功能。我尝试使用Task.WhenAny来完成此操作,如下所示。
首先,您应该知道,如果不将CancellationToken传递给GetRequestAsync,则实际上不会取消请求。它将继续处理。
其次,我发现您的代码相当奇怪,因为在其当前状态下有两个可能的超时:Task.Delay可能会完成,或者CancellationToken可能会被标记。其中一个(Task.Delay)是正常任务完成,而另一个(CancellationToken)是真正的取消。
如果CancellationToken是您的超时,则可以使用我的Nito.AsyncEx.Tasks库中的WaitAsync方法:
Task task = restService.GetRequestAsync(articleAdr, articleParams);
await task.WaitAsync(cts.Token);
var result = await task;

如果 CancellationToken 是用户请求的取消,并且 Task.Delay 是您想要应用的超时时间,那么我建议将超时建模为另一种取消方式:
Task task = restService.GetRequestAsync(articleAdr, articleParams);
using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token))
{
  timeoutCts.CancelAfter(1000);
  await task.WaitAsync(timeoutCts.Token);
}
var result = await task;

如果你不想使用 Nito.AsyncEx.Tasks,那么你最好的选择可能是像这样(假设 Task.Delay 用作超时控制而 CancellationToken 是用户取消请求):
Task task = restService.GetRequestAsync(articleAdr, articleParams);
var completed = await Task.WhenAny(task, Task.Delay(1000, cts.Token));
if (completed != task)
  throw new OperationCanceledException();
var result = await task;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接