用C#做foreach的巧妙方法

本文关键字:方法 foreach | 更新日期: 2023-09-27 17:50:44

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

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(,可以在执行过程中简单地进行增强,并且这种行为不能提前确定。当对对象(此处为job(调用方法Do()时,它可能返回要执行的新作业,从而增强源(jobs(。

我可以递归地调用这个方法(RunUntilEmpty(,但不幸的是,堆栈可能非常庞大,很可能会导致溢出。

你能告诉我如何做到这一点吗?有没有一种方法可以在C#中执行这种操作?

用C#做foreach的巧妙方法

如果我理解正确的话,您基本上是从Job对象的一些集合开始的,每个对象代表某个任务,该任务本身可以在执行其任务时创建一个或多个新的Job对象。

您更新的代码示例看起来基本上可以实现这一点。但请注意,正如评论人士CommuSoft所指出的,它不会最有效地利用您的CPU内核。因为您只是在每组作业完成后才更新作业列表,所以在所有以前生成的作业完成之前,新生成的作业无法运行。

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

我同意TPL数据流可能是实现这一点的有用方法。但是,根据您的需要,您可能会发现将任务直接排入线程池并使用CountdownEvent跟踪工作进度非常简单,这样您的RunUntilEmpty()方法就知道何时返回。

如果没有一个好的最小完整代码示例,就不可能提供包含类似完整代码示例的答案。但希望下面的片段能很好地说明基本思想:

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. 运行CCD_ 14方法
  2. 使用QueueJobs()方法对任何新任务进行排队,以便CountdownEvent对象的计数器相应地递增,并且
  3. CountdownEvent发送信号,递减当前任务的计数器

RunUntilEmpty()CountdownEvent发出信号,说明它在创建对象计数器时为其贡献的单个计数,然后等待计数器达到零。

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

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