C# 为什么在Parallel.ForEach中每个线程都会多次调用localInit Func

C# 为什么在Parallel.ForEach中每个线程都会多次调用localInit Func,c#,.net,task-parallel-library,.net-4.5,C#,.net,Task Parallel Library,.net 4.5,我正在编写一些代码来处理大量数据,我认为让Parallel.ForEach为它创建的每个线程创建一个文件会很有用,这样输出就不需要同步(至少我是这样) 它看起来像这样: Parallel.ForEach(vals, new ParallelOptions { MaxDegreeOfParallelism = 8 }, ()=>GetWriter(), // returns a new BinaryWriter backed by a file with a guid nam

我正在编写一些代码来处理大量数据,我认为让Parallel.ForEach为它创建的每个线程创建一个文件会很有用,这样输出就不需要同步(至少我是这样)

它看起来像这样:

Parallel.ForEach(vals,
    new ParallelOptions { MaxDegreeOfParallelism = 8 },
    ()=>GetWriter(), // returns a new BinaryWriter backed by a file with a guid name
    (item, state, writer)=>
    {
        if(something)
        {
            state.Break();
            return writer;
        }
        List<Result> results = new List<Result>();

        foreach(var subItem in item.SubItems)
            results.Add(ProcessItem(subItem));

        if(results.Count > 0)
        {
            foreach(var result in results)
                result.Write(writer);
        }
        return writer;
    },
    (writer)=>writer.Dispose());
我明白了:

init 10
init 14
init 11
init 13
init 12
init 14
init 11
init 12
init 13
init 11
... // hundreds of lines over < 30 seconds
init 14
init 11
init 18
init 17
init 10
init 11
init 14
init 11
init 14
init 11
init 18
init10
初始14
初始化11
初始13
初始12
初始14
初始化11
初始12
初始13
初始化11
... // 小于30秒的数百行
初始14
初始化11
初始值18
初始17
初始10
初始化11
初始14
初始化11
初始14
初始化11
初始值18
注意:如果我省略了Thread.Sleep调用,它有时似乎运行“正确”。localInit只会在它决定在我的pc上使用的4个线程中被调用一次。但是,不是每次

这是函数的期望行为吗?幕后发生了什么导致它这样做?最后,什么是获得所需功能的好方法,ThreadLocal


顺便说一句,这是在.NET4.5上。创建线程时调用了localInit。 若主体需要很长时间,它必须创建另一个线程并挂起当前线程, 如果它创建了另一个线程,它将调用localInit

此外,调用Parallel.ForEach时,它会创建与MaxDegreeOfParallelism值一样多的线程,例如:

var k = Enumerable.Range(0, 1);
Parallel.ForEach(k,new ParallelOptions(){MaxDegreeOfParallelism = 4}.....

它在第一次调用时创建了4个线程,您看到的是实现试图尽快完成您的工作

为此,它尝试使用不同数量的任务来最大化吞吐量。它从线程池中获取一定数量的线程,并运行您的工作一段时间。然后,它尝试添加和删除线程以查看发生了什么。它会继续这样做,直到你所有的工作都完成

该算法非常愚蠢,因为它不知道您的工作是否使用了大量的CPU或IO,甚至不知道是否有大量的同步,线程是否相互阻塞。它所能做的就是添加和删除线程,并测量每个工作单元完成的速度

这意味着它在注入和退出线程时不断调用
localInit
localFinally
函数,这就是您所发现的

不幸的是,没有简单的方法来控制这个算法
Parallel.ForEach
是一种高级构造,它有意隐藏大部分线程管理代码


使用
ThreadLocal
可能会有所帮助,但这取决于这样一个事实:线程池将在
并行时重用相同的线程。ForEach
请求新线程。这是无法保证的——事实上,线程池不太可能在整个调用中使用8个线程。这意味着您将再次创建超出需要的文件


保证的一件事是
并行。ForEach
在任何时候都不会使用超过
MaxDegreeOfParallelism
的线程

您可以通过创建一个固定大小的文件“池”来利用这个优势,在特定时间运行的任何线程都可以重用这些文件。您知道只有
MaxDegreeOfParallelism
线程可以同时运行,因此您可以在调用
ForEach
之前创建相同数量的文件。然后在
localnit
中抓取一个,然后在
localFinally
中释放它

当然,您必须自己编写这个池,而且它必须是线程安全的,因为它将被并发调用。不过,一个简单的锁定策略应该足够好了,因为与锁的成本相比,线程不会很快注入和失效。

根据
localInit
方法为每个任务调用一次,而不是为每个线程调用一次:

对于参与循环执行的每个任务,localInit委托将被调用一次,并返回每个任务的初始本地状态


Parallel.ForEach
不像您想象的那样工作。需要注意的是,该方法构建在
任务
类之上,并且任务和
线程
之间的关系不是1:1。例如,您可以有10个任务在2个托管线程上运行

尝试在方法正文中使用此行,而不是当前行:

Console.WriteLine("ThreadId {0} -- TaskId {1} ",
                  Thread.CurrentThread.ManagedThreadId, Task.CurrentId);
您应该看到,
ThreadId
将在许多不同的任务中重用,这些任务由其唯一的id显示。如果您继续或增加对
Thread.Sleep
的调用,您将看到更多这方面的内容

Parallel.ForEach
方法工作原理的(最基本的)基本思想是,它需要您的enumerable创建一系列任务,这些任务将运行枚举的流程部分,其执行方式在很大程度上取决于输入。还有一些特殊的逻辑,用于检查任务超过一定毫秒数而未完成的情况。如果这种情况是真的,那么可能会产生一个新任务来帮助减轻工作

如果查看中的
localinit
函数的文档,您会注意到它说它
返回每个任务的本地数据的初始状态,而不是每个线程

您可能会问,为什么会产生8个以上的任务。这个答案与上一个类似,可以在的文档中找到

从默认值更改
MaxDegreeOfParallelism
,只会限制将使用的并发任务的数量

此限制仅限于并发任务的数量,而不是在整个处理过程中创建的任务数量的硬限制。正如我前面提到的,有时会产生一个单独的任务,这会导致多次调用
localinit
函数并将数百个文件写入磁盘

写入磁盘肯定是一个有点延迟的操作,特别是当您使用同步I/O时
Console.WriteLine("ThreadId {0} -- TaskId {1} ",
                  Thread.CurrentThread.ManagedThreadId, Task.CurrentId);