为什么.AsParallel()在任务中运行时会挂起?

时间:2016-04-08 06:17:53

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

在下面的简化代码中,我生成了200个任务。每个任务都需要经过一个由锁保护的关键区域。锁内部是一个.AsParallel()语句。当我运行程序时,没有任何反应。该程序无限期挂起,没有打印任何内容。

private static object lockObject = new object();

static void Main(string[] args)
{
    RunTasks();
}

private static void RunTasks()
{
    List<Task> tasks = new List<Task>();
    for (int i = 0; i < 200; i++)
    {
        tasks.Add(Task.Factory.StartNew(PerformComputations));
    }

    Task.WaitAll(tasks.ToArray());
}

private static void PerformComputations()
{
    // Computations

    lock (lockObject)
    {
        // The actual operations performed here are irrelevant. The key is that they use .AsParallel()
        foreach (int i in Enumerable.Range(0, 500).AsParallel().Select(i => i))
        {
            Console.WriteLine(i);
        }
    }

    // Additional computations
}

但是,如果像这样实现RunTasks,一切都会正常运行(但速度很慢):

Parallel.For(0, 200, i =>
{
    PerformComputations();
});

如果我从PerformComputations中删除.AsParallel()语句,那么一切也都有效。

问题:

  1. 为什么原始代码会锁定?
    • 我最好的猜测是,RunTasks会产生200个任务,这超过了我机器上的物理核心数量。 PerformComputations中的lock语句确保阻止除一个任务之外的所有任务。当未阻塞的线程运行并行查询时,它会对另一个任务进行排队。但是,活动任务的最大数量已经处于活动状态,因此新任务将永远闲置在队列中。
    • 这是准确的吗?任何人都可以向我指出可以证实这一点或更详细解释的文档吗?
  2. 为什么修改后的RunTasks版本有效?
    • 只是Parallel.For队列小于最大活动任务数吗?
  3. 有没有办法以这样的方式编写PerformComputations,它可以使用原始的RunTasks方法,但仍然可以并行运行?

4 个答案:

答案 0 :(得分:0)

您对问题#1和#2的回答绝对正确。

回答#3:您可以在创建任务时指定TaskCreationOptions.LongRunning。根据{{​​3}}的文档,这将为任务调度程序提供提示,该任务可能需要一个额外的线程,这样它就不会阻止其他线程或工作的前进本地线程池队列中的项目。

实际上,这会使任务系统忽略ThreadPool,只为您的任务提供一个新的专用线程。

答案 1 :(得分:0)

Parallel.For和.ForEach方法以及System.Collection.Concurrent命名空间确实让您的生活更容易处理这类问题。调度程序根据进程优先级,系统工作负载,内核数量等处理您的线程管理...并行性变得简单:

    static void Main(string[] args)
    {
        RunTasks();
    }

    // This sets up the parallel scheduler to use UP TO 16 simultaneous threads. In reality the thread
    // workload is managed by the CLR according to how many logical threads you have available on your
    // processor.
    private static readonly ParallelOptions _po = new ParallelOptions() { MaxDegreeOfParallelism = 16 };

    private static void RunTasks()
    {
        // Run 200 instances of PerformComputations in parallel. 
        Parallel.For(0, 200, _po, i => PerformComputations());
    }

    private static void PerformComputations()
    {
        // If you want to run the 500 iterations in parallel (sequence is not important),
        // use a concurrent collection. This needs absolutely no lock, the collection is
        // partitioned internally to avoid having to lock. Same goes if you need to share
        // data between multiple runs of PerformComputations(), declare a static bag at 
        // class level.
        var theBag = new ConcurrentBag<int>(Enumerable.Range(0, 500));
        Parallel.ForEach(theBag, _po, i =>
        {
            Console.WriteLine(i.ToString());
        });

        // Otherwise you don't need a lock at all anyway since each element here is treated
        // one at a time in sequence.
        var theList = Enumerable.Range(0, 500).ToList();
        foreach (var i in theList)
        {
            Console.WriteLine(i.ToString());
        }
    }

答案 2 :(得分:0)

是的,你是对的 - 原始代码锁定在PerformComputations的并行部分。

LongRunning强制创建一个全新的非线程池线程(告诉调度程序为该任务创建一个新线程)。注意:您可能会创建许多线程,从而导致内存开销和切换开销等问题。

private static void RunTasks()
{
    List<Task> tasks = new List<Task>();
    for (int i = 0; i < maxLoops; i++)
    {
        tasks.Add(Task.Factory.StartNew(PerformComputations, TaskCreationOptions.LongRunning));
    }

    Task.WaitAll(tasks.ToArray());
}

有趣的阅读:Parallelism in .NET

回答问题3:如果你不介意创建多个线程(使用Parallel.For)vs组合结果(AsParallel().Select)。

private static void PerformComputations()
{
    lock (lockObject)
    {
        Parallel.For(0, 500, i =>
        {
            Console.WriteLine(i);
        });
    }
}

答案 3 :(得分:0)

首先,我不明白您为什么要使用AsParallel()。如果您有200个大多数独立的Task,那么它应足以充分利用您的CPU。这尤其令人困惑,因为AsParallel()并行执行的唯一操作是无用的Select()

现在,要真正回答你的问题:

  

我最好的猜测是,RunTasks会产生200个任务,这超过了我机器上的物理核心数量。

核心数量不相关。可用线程数更重要。 TPL使用ThreadPool,它限制了每秒创建的线程数,也是线程总数的硬限制。如果达到第一个限制,您的代码可能会变慢为爬行(并且显示不执行任何操作)。如果达到第二个限制,您的代码实际上会死锁并停止工作。

第一个限制不可配置或记录良好,the second limit is

在任何情况下,达到这些限制中的任何一个都表明您的代码在并行性方面设计糟糕。

  

为什么修改后的RunTasks版本有效?它只是Parallel.For队列小于最大活动任务数吗?

是的,Parallel.For使用较少数量的Task,因为效率更高。

  

有没有办法以这样的方式编写PerformComputations,它可以使用原始的RunTasks方法,但仍然可以并行运行?

我不明白你为什么要那样做。就像我之前说的那样,我不认为并行运行Select()是有道理的。