循环枚举器的PLINQ迭代导致死锁

时间:2016-11-29 18:21:25

标签: c# .net parallel-processing task-parallel-library plinq

我有一个简单的程序,它迭代了作为反馈枚举器实现的无数可枚举。我已在TPL和PLINQ中实现了这一点。两个示例在可预测的迭代次数后锁定:PLINQ为8,TPL为3。代码是在不使用TPL / PLINQ的情况下执行的,运行正常。我已经以非线程安全的方式实现了枚举器以及线程安全的方式。如果并行度限制为1,则可以使用前者(如示例中的情况)。非线程安全的枚举器非常简单,不依赖于任何“花哨的”.NET库类。如果我增加并行度,则在死锁之前执行的迭代次数增加,例如对于PLINQ,迭代次数是8 *并行度。

以下是迭代器:
枚举器(非线程安全)

public class SimpleEnumerable<T>: IEnumerable<T>
{
    private T _value;
    private readonly AutoResetEvent _releaseValueEvent = new AutoResetEvent(false);

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public IEnumerator<T> GetEnumerator()
    {
        while(true)
        {
            _releaseValueEvent.WaitOne();
            yield return _value;
        }
    }

    public void OnNext(T value)
    {
        _value = value;
        _releaseValueEvent.Set();
    }
}

枚举器(线程安全)

public class SimpleEnumerable<T>: IEnumerable<T>
{
    private readonly BlockingCollection<T> _blockingCollection = new BlockingCollection<T>();

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public IEnumerator<T> GetEnumerator()
    {
        while(true)
        {
            yield return _blockingCollection.Take();
        }
    }

    public void OnNext(T value)
    {
        _blockingCollection.Add(value);
    }
}

PLINQ示例:

public static void Main(string[] args)
{
    var enumerable = new SimpleEnumerable<int>();
    enumerable.OnNext(0);

    enumerable
        .Do(i => Debug.WriteLine($"{i} {Thread.CurrentThread.ManagedThreadId}"))
        .AsParallel()
        .WithDegreeOfParallelism(1)
        .ForEach
        (
            i =>
            {
                Debug.WriteLine($"{i} {Thread.CurrentThread.ManagedThreadId}");
                enumerable.OnNext(i+1);
            }
        );
}

TPL示例:

public static void Main(string[] args)
{
    var enumerable = new SimpleEnumerable<int>();
    enumerable.OnNext(0);

    Parallel.ForEach
    (
        enumerable,
        new ParallelOptions { MaxDegreeOfParallelism = 1},
        i =>
        {
            Debug.WriteLine($"{i} {Thread.CurrentThread.ManagedThreadId}");
            enumerable.OnNext(i+1);
        }
    );
}

根据我对callstack的分析,似乎在PLINQ和TPL中的分区相关方法中都存在死锁,但我不确定如何解释它。

通过反复试验,我发现enumerable中的PLINQ Partitioner.Create(enumerable, EnumerablePartitionerOptions.NoBuffering)包裹修复了问题,但我不确定为什么会发生死锁。

我非常有兴趣找出错误的根本原因。

请注意,这是一个人为的例子。我不是在寻找对代码的批评,而是为什么是发生的死锁。具体来说,在PLINQ示例中,如果注释掉.AsParallel().WithDegreeOfParallelism(1)行,则代码可以正常工作。

1 个答案:

答案 0 :(得分:2)

您实际上并没有逻辑的值序列,因此首先尝试创建IEnumerable根本没有任何意义。此外,您几乎肯定不会尝试创建可由多个线程使用的IEnumerator。存在着疯狂,仅仅是因为IEnumerator暴露的界面并没有真正暴露出你想要的东西才能做到这一点。您可以创建一个IEnumerator,它只会由单个线程使用,该线程根据多个线程使用的基础数据源计算要返回的数据,因为它非常不同。 / p>

如果您只是想创建一个在不同线程中运行的生产者和消费者,请不要创建自己的&#34;包装器&#34;在BlockingCollection附近,*只需使用BlockingCollection。让生产者添加它,消费者从中读取。消费者可以使用GetConsumingEnumerable,如果它只想在获取这些项目时迭代项目(想要做的常见操作)。