在 C# 中执行 foreach 的棘手方法
Tricky way of doing foreach in C#
我会先提供伪代码并描述如下:
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
),它可能 return 执行新作业,从而增强源代码(jobs
)。
我可以递归调用此方法 (RunUntilEmpty
),但不幸的是,堆栈可能非常大,很可能导致溢出。
你能告诉我如何实现吗?有没有办法在 C# 中执行此类操作?
如果我理解正确的话,你基本上是从一些 Job
对象的集合开始的,每个对象代表一些任务,这些任务本身可以创建一个或多个新的 Job
对象作为执行其任务的结果.
您更新的代码示例看起来基本上可以完成此操作。但请注意,正如评论员 CommuSoft 指出的那样,它不会最有效地利用您的 CPU 核心。因为您只是在每组作业完成后才更新作业列表,所以新生成的作业无法 运行 直到 all 之前生成的作业完成完成。
更好的实施方式是使用单个作业队列,在旧对象完成时不断检索新的 Job
对象以供执行。
我同意 TPL 数据流可能是实现此目的的有用方法。然而,根据您的需要,您可能会发现直接将任务排队到线程池并使用 CountdownEvent
跟踪工作进度这样您的 RunUntilEmpty()
方法就知道何时 return.
没有 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
的计数器。任务本身做三件事:
- 运行
Do()
方法,
- 使用
QueueJobs()
方法对任何新任务进行排队,以便 CountdownEvent
对象的计数器相应增加,并且
- 向
CountdownEvent
发送信号,递减当前任务的计数器
主 RunUntilEmpty()
向 CountdownEvent
发出信号,说明它在创建对象时对对象计数器的贡献,然后等待计数器归零。
请注意,对 QueueJobs()
的调用 不是 递归的。 QueueJobs()
方法本身不调用,而是由其中声明的匿名方法调用,QueueJobs()
本身也不调用该方法。所以这里没有堆栈溢出问题。
上面的关键特征是任务在已知时连续排队,即因为它们被先前执行的 Do()
方法调用 return 编辑。因此,可用的 CPU 核心由线程池保持忙碌,至少在某种程度上,任何已完成的 Do()
方法实际上已经 returned 任何新的 Job
对象运行。这解决了您在问题中包含的代码版本的主要问题。
我会先提供伪代码并描述如下:
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
),它可能 return 执行新作业,从而增强源代码(jobs
)。
我可以递归调用此方法 (RunUntilEmpty
),但不幸的是,堆栈可能非常大,很可能导致溢出。
你能告诉我如何实现吗?有没有办法在 C# 中执行此类操作?
如果我理解正确的话,你基本上是从一些 Job
对象的集合开始的,每个对象代表一些任务,这些任务本身可以创建一个或多个新的 Job
对象作为执行其任务的结果.
您更新的代码示例看起来基本上可以完成此操作。但请注意,正如评论员 CommuSoft 指出的那样,它不会最有效地利用您的 CPU 核心。因为您只是在每组作业完成后才更新作业列表,所以新生成的作业无法 运行 直到 all 之前生成的作业完成完成。
更好的实施方式是使用单个作业队列,在旧对象完成时不断检索新的 Job
对象以供执行。
我同意 TPL 数据流可能是实现此目的的有用方法。然而,根据您的需要,您可能会发现直接将任务排队到线程池并使用 CountdownEvent
跟踪工作进度这样您的 RunUntilEmpty()
方法就知道何时 return.
没有 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
的计数器。任务本身做三件事:
- 运行
Do()
方法, - 使用
QueueJobs()
方法对任何新任务进行排队,以便CountdownEvent
对象的计数器相应增加,并且 - 向
CountdownEvent
发送信号,递减当前任务的计数器
主 RunUntilEmpty()
向 CountdownEvent
发出信号,说明它在创建对象时对对象计数器的贡献,然后等待计数器归零。
请注意,对 QueueJobs()
的调用 不是 递归的。 QueueJobs()
方法本身不调用,而是由其中声明的匿名方法调用,QueueJobs()
本身也不调用该方法。所以这里没有堆栈溢出问题。
上面的关键特征是任务在已知时连续排队,即因为它们被先前执行的 Do()
方法调用 return 编辑。因此,可用的 CPU 核心由线程池保持忙碌,至少在某种程度上,任何已完成的 Do()
方法实际上已经 returned 任何新的 Job
对象运行。这解决了您在问题中包含的代码版本的主要问题。