【发布时间】:2017-04-13 20:53:30
【问题描述】:
我有一个简单的程序,它迭代一个作为反馈枚举器实现的无限枚举。我已经在 TPL 和 PLINQ 中实现了这一点。这两个示例都在可预测的迭代次数后锁定:PLINQ 8 次,TPL 3 次。如果代码在不使用 TPL/PLINQ 的情况下执行,它运行良好。我以非线程安全的方式和线程安全的方式实现了枚举器。如果并行度限制为一个(如示例中的情况),则可以使用前者。非线程安全枚举器非常简单,不依赖于任何“花哨”的 .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);
}
);
}
根据我对调用堆栈的分析,似乎在 PLINQ 和 TPL 中的分区程序相关方法中都发生了死锁,但我不确定如何解释。
通过反复试验,我发现将 PLINQ enumerable 包装在 Partitioner.Create(enumerable, EnumerablePartitionerOptions.NoBuffering) 中可以解决问题,但我不确定为什么会发生死锁。
我很想找出错误的根本原因。
请注意,这是一个人为的示例。我不是在寻找对代码的批评,而是为什么会发生死锁。具体来说,在 PLINQ 示例中,如果 .AsParallel() 和 .WithDegreeOfParallelism(1) 行被注释掉,代码就可以正常工作。
【问题讨论】:
-
PLINQ 和 Parallel 不会造成死锁,它们使用当前线程和其他 N 个线程并行处理数据
-
@PanagiotisKanavos 显然死锁在他的迭代器中。乍一看,我并不感到惊讶,它肯定看起来一点也不安全。
-
@Servy 我从最明显的问题开始。迭代器 ... 是一个非常非常奇怪的构造。一个简单的 10K 整数数组足以测试并行执行。
Interlocked.Increment将是一个很好的计数方式。这个迭代器,只是阻塞 -
你的代码并没有真正做任何事情。在不知道它应该做什么的情况下,人们无法真正说出如何正确地完成它。
-
这段代码唯一显示的是阻塞在单个线程上的迭代器是坏的。 实际问题是什么?
标签: c# .net parallel-processing task-parallel-library plinq