在C#中做foreach的棘手方法

时间:2015-05-24 14:14:24

标签: c# foreach parallel-processing

我将首先提供伪代码并在下面进行描述:

public void RunUntilEmpty(List<Job> jobs)
{
    while (jobs.Any()) // the list "jobs" will be modified during the execution
    {
        List<Job> childJobs = new List<Job>();

        Parallel.ForEach(jobs, job => // this will be done in parallel
        {
            List<Job> newJobs = job.Do(); // after a job is done, it may return new jobs to do
            lock (childJobs)
                childJobs.AddRange(newJobs); // I would like to add those jobs to the "pool"
        });

        jobs = childJobs;
    }
}

如您所见,我正在执行一种独特的foreach类型。源(集合jobs)可以在执行期间简单地增强,并且此行为无法在之前确定。在对象(此处为Do())上调用方法job时,它可能会返回要执行的新作业,从而增强源(jobs)。

我可以递归地调用这个方法(RunUntilEmpty),但遗憾的是堆栈可能非常庞大并且很可能导致溢出。

你能告诉我如何实现这个目标吗?有没有办法在C#中做这种行为?

1 个答案:

答案 0 :(得分:2)

如果我理解正确,你基本上会从一些Job个对象开始,每个对象代表一些任务,这些任务本身可以在执行任务时创建一个或多个新的Job个对象。 / p>

您的更新代码示例看起来基本上可以实现此目的。但请注意,正如CommuSoft指出的那样,它不会最有效地利用您的CPU内核。因为您只是在每组作业完成后更新作业列表,所以在所有以前生成的作业完成之前,新生成的作业无法运行。< / p>

更好的实现将使用单个作业队列,不断检索新的Job对象,以便在旧对象完成时执行。

我同意TPL Dataflow可能是实现此目的的有用方法。但是,根据您的需要,您可能会发现它很简单,只需将任务直接排队到线程池,并使用CountdownEvent跟踪工作进度,以便您的RunUntilEmpty()方法知道何时返回

如果没有a good, minimal, complete code example,则无法提供包含类似完整代码示例的答案。但希望下面的代码片段能够很好地说明基本思想:

public void RunUntilEmpty(List<Job> jobs)
{
    CountdownEvent countdown = new CountdownEvent(1);

    QueueJobs(jobs, countdown);

    countdown.Signal();
    countdown.Wait();
}

private static void QueueJobs(List<Job> jobs, CountdownEvent countdown)
{
    foreach (Job job in jobs)
    {
        countdown.AddCount(1);

        Task.Run(() =>
        {
            // after a job is done, it may return new jobs to do
            QueueJobs(job.Do(), countdown);

            countdown.Signal();
        });
    }
}

基本思想是为每个Job对象排队一个新任务,为每个排队的任务递增CountdownEvent的计数器。任务本身做了三件事:

  1. 运行Do()方法,
  2. 使用QueueJobs()方法对任何新任务进行排队,以便CountdownEvent对象的计数器相应增加,并且
  3. 发信号通知CountdownEvent,递减其当前任务的计数器
  4. RunUntilEmpty()CountdownEvent发出信号,说明它在创建对象计数器时对其提供的单个计数,然后等待计数器达到零。

    请注意,对QueueJobs()的调用是递归的。 QueueJobs()方法本身不是调用,而是由在其中声明的匿名方法调用,它本身也不由QueueJobs()调用。所以这里没有堆栈溢出问题。

    上面的关键特性是任务在知道时连续排队,即由先前执行的Do()方法调用返回。因此,线程池使可用的CPU核心保持忙碌,至少在任何已完成的Do()方法实际上已返回任何要运行的新Job对象的范围内。这解决了您在问题中包含的代码版本的主要问题。