所以情况就是这样:我需要拨打一个开始搜索的网站。这种搜索会持续一段时间,我知道搜索完成的唯一方法是定期查询网站,查看网站上是否有“下载数据”链接(它在javascript上使用了一些奇怪的ajax调用)用于检查后端并更新页面的计时器,我认为。)
所以这就是诀窍:我需要搜索数百个项目,一次一个。所以我有一些看起来有点像这样的代码:
var items = getItems();
Parallel.ForEach(items, item =>
{
startSearch(item);
var finished = isSearchFinished(item);
while(finished == false)
{
finished = isSearchFinished(item); //<--- How do I delay this action 30 Secs?
}
downloadData(item);
}
现在,显然这不是真正的代码,因为可能会导致isSearchFinished
始终为false
。
除了显而易见的无限循环危险之外,我如何正确地让isSearchFinished()
无法一次又一次地调用,而是每次拨打30秒或1分钟?
我知道Thread.Sleep()
不是正确的解决方案,我认为解决方案可能是通过使用Threading.Timer()
来完成的,但我对它并不是很熟悉,并且有很多线程选项可供选择我只是不确定要使用哪个。
答案 0 :(得分:9)
正如@KevinS在评论中指出的那样,使用任务和async/await
很容易实现:
async Task<ItemData> ProcessItemAsync(Item item)
{
while (true)
{
if (await isSearchFinishedAsync(item))
break;
await Task.Delay(30 * 1000);
}
return await downloadDataAsync(item);
}
// ...
var items = getItems();
var tasks = items.Select(i => ProcessItemAsync(i)).ToArray();
await Task.WhenAll(tasks);
var data = tasks.Select(t = > t.Result);
通过这种方式,您不会因为大多数I / O绑定的网络操作而无法阻塞ThreadPool
线程。如果您不熟悉async/await
,那么async-await
标记维基可能是一个很好的起点。
我假设您可以使用类似isSearchFinished
的内容将同步方法downloadData
和HttpClient
转换为异步版本,以用于非阻塞HTTP请求并返回Task<>
。如果您无法这样做,您仍然可以使用Task.Run
,await Task.Run(() => isSearchFinished(item))
和await Task.Run(() => downloadData(item))
将其打包。通常不推荐这样做,但是因为你有数百个项目,所以在这种情况下,它会比Parallel.ForEach
提供更好的并发性,因为你不会阻塞30秒的池线程,感谢异步Task.Delay
。
答案 1 :(得分:3)
您还可以使用TaskCompletionSource
和Threading.Timer
编写通用函数,以便在指定的重试功能成功后返回完成的Task
。
public static Task RetryAsync(Func<bool> retryFunc, TimeSpan retryInterval)
{
return RetryAsync(retryFunc, retryInterval, CancellationToken.None);
}
public static Task RetryAsync(Func<bool> retryFunc, TimeSpan retryInterval, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<object>();
cancellationToken.Register(() => tcs.TrySetCanceled());
var timer = new Timer((state) =>
{
var taskCompletionSource = (TaskCompletionSource<object>) state;
try
{
if (retryFunc())
{
taskCompletionSource.TrySetResult(null);
}
}
catch (Exception ex)
{
taskCompletionSource.TrySetException(ex);
}
}, tcs, TimeSpan.FromMilliseconds(0), retryInterval);
// Once the task is complete, dispose of the timer so it doesn't keep firing. Also captures the timer
// in a closure so it does not get disposed.
tcs.Task.ContinueWith(t => timer.Dispose(),
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
return tcs.Task;
}
然后您可以像这样使用RetryAsync
:
var searchTasks = new List<Task>();
searchTasks.AddRange(items.Select(
downloadItem => RetryAsync( () => isSearchFinished(downloadItem), TimeSpan.FromSeconds(2)) // retry timout
.ContinueWith(t => downloadData(downloadItem),
CancellationToken.None,
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.Default)));
await Task.WhenAll(searchTasks.ToArray());
ContinueWith
部分指定在任务成功完成后您执行的操作。在这种情况下,它将在线程池线程上运行您的downloadData
方法,因为我们指定了TaskScheduler.Default
并且只有在任务运行完成时才会执行,即它没有被取消且没有抛出异常。