使用.NET .4.5.1中的执行图进行适当的线程池和并行化

时间:2015-04-08 06:31:42

标签: c# multithreading threadpool

我有一个像这样的当前算法。

public class Executor
{ 
    private ParallelOptions options = new ParallelOptions();
    private IList<Step> AllSteps;
    public void Execute()
    {
        options.MaxDegreeOfParallelism = 4;
        var rootSteps = AllSteps.Where(s => !s.Parents.Any());
        Parallel.Foreach(rootSteps, options, RecursivelyExecuteStep);
    }   
    private void RecursivelyExecuteStep(Step step)
    {
        ExecuteStep();
        var childSteps = AllSteps.Where(s=>s.Parents.Contains(step) 
            && step.Parents.All(p=>p.IsComplete);
        Parallel.ForEach(childSteps, options, RecursivelyExecuteStep);
    }
}

ParallelOptions.MaxDegreeOfParallelism将是一个输入变量(但为了简洁,将其排除在代码示例之外)。

我想知道是否自动为我处理线程池,或者每次都创建新线程。另外,优化这一点的最佳方式是线程池化我想要的东西。我如何使用线程池。我是多线程的新手,4.5 [.1]

中的新功能

这不会将算法限制为只有4个线程,因为每个Parallel.Foreach都拥有自己的MaxDegreeOfParallelism 4,因此不会将应用程序中的所有线程限制为4吗?如何将应用程序中的所有线程限制为4?

编辑:MaxDegreeOfParallelism

3 个答案:

答案 0 :(得分:1)

Parallel.Foreach基本上是将工作项排队到.NET ThreadPool的好方法。

您的应用程序(进程)只有ThreadPool的一个实例,它会尽可能地了解它使用多少并发线程,并考虑可用内核和虚拟内存的数量。

是的,.NET ThreadPool为您处理线程池,在许多情况下您不必担心它,使用Parallel.Foreach并让它继续使用它。

编辑:正如其他人所指出的,你应该小心过度使用ThreadPool,因为它是一个共享资源,它可能会干扰你的应用程序的其他部分。如果您的项目阻塞或长时间运行,它也将开始创建新线程,这通常是浪费。根据经验,工作项应该相对较快,最好是非阻塞的。您应该进行测试和基准测试,如果它适用于您的用例,则非常方便。

如果要进行显式控制,可以通过调用ThreadPool来控制应用程序中ThreadPool.SetMaxThreads使用的最大并发线程数。除非你真的需要,并且知道你在做什么,否则我建议不要这样做。例如,ThreadPool已经尝试避免使用比核心更多的并发线程。

使用ParallellOptions.MaxDegreeOfParallelism可以做的只是进一步限制用于执行对Parallel.Foreach的特定调用的并发ThreadPool线程的数量。

如果您需要更加明确地控制算法调用使用的并发线程数,这里有一些可能的替代方案(可以说,增加实现复杂性):

  • 使用默认的ThreadPool,您无法在递归调用Parellel.Foreach时限制并发性。例如,您可以考虑仅在顶层使用Parallel.Foreach(使用ParellelOptions.MaxDegreeOfParallelism参数)并让RecursivelyExecuteStep使用标准foreach。
  • 修改(或替换)算法的ThreadPool以限制并发性,方法是将ParallelOptions.TaskScheduler设置为QueuedTaskScheduler的{​​{1}}实例,如here所述。
  • 根据@VMAtm的建议,您可以使用TPL Dataflow获取更多信息 明确控制计算的执行方式,包括 并发(如果你这也可以与自定义任务调度程序结合使用) 真的很想把自己搞得一团糟。)

答案 1 :(得分:1)

您可以使用TPL DataFlow库解决此问题(您可以通过NuGet获取)。正如在其他答案中所说,Parallel类在内部使用ThreadPool,你不应该为此烦恼。

使用TPL数据流,您唯一需要的是创建一个TransformManyBlock<TInput,TOutput>自身链接(或link BufferBlock with ActionBlock with Encapsulate extension),并设置MaxDegreeOfParallelism = 4或您认为应该是的任何常量

答案 2 :(得分:0)

简单直接的实现可能如下所示:

ParallelOptions Options = new ParallelOptions{MaxDegreeOfParallelism = 4};
IList<Step> AllSteps;

public void Execute()
{
    var RemainingSteps = new HashSet<Step>(AllSteps);

    while(RemainingSteps.Count > 0)
    {
        var ExecutableSteps = RemainingSteps.Where(s => s.Parents.All(p => p.IsComplete)).ToList();

        Parallel.ForEach(ExecutableSteps, Options, ExecuteStep);

        RemainingSteps.ExceptWith(ExecutableSteps);
    }   
}

当然,这将分阶段执行步骤,因此您不会总是具有最大并发性。您可能只在每个阶段结束时执行一个步骤,因为执行的后续步骤仅在当前阶段的所有步骤完成后才会实现。

如果您想提高并发性,我建议您使用BlockingCollection。在这种情况下,您需要implement a custom partitioner对阻止集合使用Parallel.ForEach。您还需要同时收集剩余的步骤,这样您就不会多次排队同一步(先前评论过的竞争条件)。

public class Executor
{ 
    ParallelOptions Options = new ParallelOptions() { MaxDegreeOfParallelism = 4 };

    IList<Step> AllSteps;

    //concurrent hashset of remaining steps (used to prevent race conditions)
    ConcurentDictionary<Step, Step> RemainingSteps = new ConcurentDictionary<Step, Step>();

    //blocking collection of steps that can execute next
    BlockingCollection<Step> ExecutionQueue = new BlockingCollection<Step>();

    public void Execute()
    {
        foreach(var step in AllSteps)
        {
            if(step.Parents.All(p => p.IsComplete))
            {
                ExecutionQueue.Add(step);
            }
            else
            {
                RemainingSteps.Add(step, step);
            }
        }

        Parallel.ForEach(
            GetConsumingPartitioner(ExecutionQueue),
            Options,
            Execute);
    }

    void Execute(Step step)
    {
        ExecuteStep(step);

        if(RemainingSteps.IsEmpty)
        {
            //we're done, all steps are complete
            executionQueue.CompleteAdding();
            return;
        }

        //queue up the steps that can execute next (concurrent dictionary enumeration returns a copy, so subsequent removal is safe)
        foreach(var step in RemainingSteps.Values.Where(s => s.Parents.All(p => p.IsComplete)))
        {
            //note, removal only occurs once, so this elimiates the race condition
            Step NextStep;
            if(RemainingSteps.TryRemove(step, out NextStep))
            {
                executionQueue.Add(NextStep);
            }
        }
    }

    Partitioner<T> GetConsumingPartitioner<T>(BlockingCollection<T> collection)
    {
        return new BlockingCollectionPartitioner<T>(collection);
    }

    class BlockingCollectionPartitioner<T> : Partitioner<T>
    {
        readonly BlockingCollection<T> Collection;

        public BlockingCollectionPartitioner(BlockingCollection<T> collection)
        {
            if (collection == null) throw new ArgumentNullException("collection");

            Collection = collection;
        }

        public override bool SupportsDynamicPartitions { get { return true; } }

        public override IList<IEnumerator<T>> GetPartitions(int partitionCount)
        {
            if (partitionCount < 1) throw new ArgumentOutOfRangeException("partitionCount");

            var Enumerable = GetDynamicPartitions();

            return Enumerable.Range(0, partitionCount)
                             .Select(i => Enumerable.GetEnumerator()).ToList();
        }

        public override IEnumerable<T> GetDynamicPartitions()
        {
            return Collection.GetConsumingEnumerable();
        }
    }
}