Parallel.ForEach使用Thread.Sleep等效

时间:2014-06-28 00:45:14

标签: c# multithreading task-parallel-library parallel.foreach

所以情况就是这样:我需要拨打一个开始搜索的网站。这种搜索会持续一段时间,我知道搜索完成的唯一方法是定期查询网站,查看网站上是否有“下载数据”链接(它在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()来完成的,但我对它并不是很熟悉,并且有很多线程选项可供选择我只是不确定要使用哪个。

2 个答案:

答案 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的内容将同步方法downloadDataHttpClient转换为异步版本,以用于非阻塞HTTP请求并返回Task<>。如果您无法这样做,您仍然可以使用Task.Runawait Task.Run(() => isSearchFinished(item))await Task.Run(() => downloadData(item))将其打包。通常不推荐这样做,但是因为你有数百个项目,所以在这种情况下,它会比Parallel.ForEach提供更好的并发性,因为你不会阻塞30秒的池线程,感谢异步Task.Delay

答案 1 :(得分:3)

您还可以使用TaskCompletionSourceThreading.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并且只有在任务运行完成时才会执行,即它没有被取消且没有抛出异常。