我正在测试自编写元素生成器(ICollection<string>
)并将计算出的计数与实际计数进行比较,以便了解我的算法中是否存在错误。 < / p>
由于这个生成器可以根据需要生成大量元素,我正在查看Partitioner<string>
并且我已经实现了一个基本的,它似乎也生成了有效的枚举器,它们共同提供了相同数量的string
s按计算。
现在我想测试一下如果运行并行(再次首次测试正确计数)的行为:
MyGenerator generator = new MyGenerator();
MyPartitioner partitioner = new MyPartitioner(generator);
int isCount = partitioner.AsParallel().Count();
int shouldCount = generator.Count;
bool same = isCount == shouldCount; // false
我不明白为什么这个数量不相等! ParallelQuery<string>
在做什么?
generator.Count() == generator.Count // true
partitioner.GetPartitions(xyz).Select(enumerator =>
{
int count = 0;
while (enumerator.MoveNext())
{
count++;
}
return count;
}).Sum() == generator.Count // true
所以,我目前没有在代码中看到错误。接下来,我尝试手动计算ParallelQuery<string>
:
int count = 0;
partitioner.AsParallel().ForAll(e => Interlocked.Increment(ref count));
count == generator.Count // true
总结:每个人都认为我的可枚举正确,ParallelQuery.ForAll
精确枚举了generator.Count
个元素。 但是ParallelQuery.Count()
是什么?
如果正确的计数约为10k,则ParallelQuery会看到40k。
internal sealed class PartialWordEnumerator : IEnumerator<string>
{
private object sync = new object();
private readonly IEnumerable<char> characters;
private readonly char[] limit;
private char[] buffer;
private IEnumerator<char>[] enumerators;
private int position = 0;
internal PartialWordEnumerator(IEnumerable<char> characters, char[] state, char[] limit)
{
this.characters = new List<char>(characters);
this.buffer = (char[])state.Clone();
if (limit != null)
{
this.limit = (char[])limit.Clone();
}
this.enumerators = new IEnumerator<char>[this.buffer.Length];
for (int i = 0; i < this.buffer.Length; i++)
{
this.enumerators[i] = SkipTo(state[i]);
}
}
private IEnumerator<char> SkipTo(char c)
{
IEnumerator<char> first = this.characters.GetEnumerator();
IEnumerator<char> second = this.characters.GetEnumerator();
while (second.MoveNext())
{
if (second.Current == c)
{
return first;
}
first.MoveNext();
}
throw new InvalidOperationException();
}
private bool ReachedLimit
{
get
{
if (this.limit == null)
{
return false;
}
for (int i = 0; i < this.buffer.Length; i++)
{
if (this.buffer[i] != this.limit[i])
{
return false;
}
}
return true;
}
}
public string Current
{
get
{
if (this.buffer == null)
{
throw new ObjectDisposedException(typeof(PartialWordEnumerator).FullName);
}
return new string(this.buffer);
}
}
object IEnumerator.Current
{
get { return this.Current; }
}
public bool MoveNext()
{
lock (this.sync)
{
if (this.position == this.buffer.Length)
{
this.position--;
}
if (this.position == -1)
{
return false;
}
IEnumerator<char> enumerator = this.enumerators[this.position];
if (enumerator.MoveNext())
{
this.buffer[this.position] = enumerator.Current;
this.position++;
if (this.position == this.buffer.Length)
{
return !this.ReachedLimit;
}
else
{
return this.MoveNext();
}
}
else
{
this.enumerators[this.position] = this.characters.GetEnumerator();
this.position--;
return this.MoveNext();
}
}
}
public void Dispose()
{
this.position = -1;
this.buffer = null;
}
public void Reset()
{
throw new NotSupportedException();
}
}
public override IList<IEnumerator<string>> GetPartitions(int partitionCount)
{
IEnumerator<string>[] enumerators = new IEnumerator<string>[partitionCount];
List<char> characters = new List<char>(this.generator.Characters);
int length = this.generator.Length;
int characterCount = this.generator.Characters.Count;
int steps = Math.Min(characterCount, partitionCount);
int skip = characterCount / steps;
for (int i = 0; i < steps; i++)
{
char c = characters[i * skip];
char[] state = new string(c, length).ToCharArray();
char[] limit = null;
if ((i + 1) * skip < characterCount)
{
c = characters[(i + 1) * skip];
limit = new string(c, length).ToCharArray();
}
if (i == steps - 1)
{
limit = null;
}
enumerators[i] = new PartialWordEnumerator(characters, state, limit);
}
for (int i = steps; i < partitionCount; i++)
{
enumerators[i] = Enumerable.Empty<string>().GetEnumerator();
}
return enumerators;
}
答案 0 :(得分:2)
编辑:我相信我找到了解决方案。根据{{3}}(强调我的)的文件:
如果MoveNext传递集合的末尾,则枚举器为 位于集合中的最后一个元素和MoveNext之后 返回false。当调查员处于此位置时,后续 调用MoveNext也会返回false,直到调用Reset 。
根据以下逻辑:
private bool ReachedLimit
{
get
{
if (this.limit == null)
{
return false;
}
for (int i = 0; i < this.buffer.Length; i++)
{
if (this.buffer[i] != this.limit[i])
{
return false;
}
}
return true;
}
}
对MoveNext()
的调用只会返回false一次 - 当缓冲区完全等于限制时。一旦超过限制,ReachedLimit
的返回值将再次开始变为假,使return !this.ReachedLimit
返回true,因此枚举器将一直超过限制的结尾,直到它用完为止要枚举的字符数。显然,在ParallelQuery.Count()
的实现中,MoveNext()
在到达结束时被多次调用,并且由于它再次开始返回一个真值,因此枚举器很高兴继续返回更多元素(这不是您的自定义代码中的情况是手动遍历枚举器,而ForAll
调用显然也不是这样,因此他们“意外地”返回正确的结果。)
最简单的解决方法是记住MoveNext()
一旦变为假的返回值:
private bool _canMoveNext = true;
public bool MoveNext()
{
if (!_canMoveNext) return false;
...
if (this.position == this.buffer.Length)
{
if (this.ReachedLimit) _canMoveNext = false;
...
}
现在一旦它开始返回false,它将为每个将来的调用返回false,这将从AsParallel().Count()
返回正确的结果。希望这有帮助!
关于IEnumerable.MoveNext
笔记的文件(强调我的):
分区程序上的静态方法都是线程安全的 从多个线程同时使用。然而,虽然创造了 分区程序正在使用中,基础数据源不应该使用 修改,无论是来自使用分区程序的相同线程还是 来自一个单独的主题。
从我对你所提供的代码的理解,似乎ParallelQuery.Count()
最有可能出现线程安全问题,因为它可能同时迭代多个枚举器,而所有其他解决方案将要求枚举器运行同步。如果没有看到您用于MyGenerator
和MyPartitioner
的代码,则难以确定线程安全问题是否可能是罪魁祸首。
为了演示,我编写了一个简单的枚举器,它将前100个数字作为字符串返回。此外,我有一个分区程序,它将基础枚举器中的元素分布在numPartitions
个单独列表的集合上。在我们的12核服务器上使用您在上面描述的所有方法(当我输出numPartitions
时,它在此机器上默认使用12
),我得到100的预期结果(这是LINQPad准备好的代码):
void Main()
{
var partitioner = new SimplePartitioner(GetEnumerator());
GetEnumerator().Count().Dump();
partitioner.GetPartitions(10).Select(enumerator =>
{
int count = 0;
while (enumerator.MoveNext())
{
count++;
}
return count;
}).Sum().Dump();
var theCount = 0;
partitioner.AsParallel().ForAll(e => Interlocked.Increment(ref theCount));
theCount.Dump();
partitioner.AsParallel().Count().Dump();
}
// Define other methods and classes here
public IEnumerable<string> GetEnumerator()
{
for (var i = 1; i <= 100; i++)
yield return i.ToString();
}
public class SimplePartitioner : Partitioner<string>
{
private IEnumerable<string> input;
public SimplePartitioner(IEnumerable<string> input)
{
this.input = input;
}
public override IList<IEnumerator<string>> GetPartitions(int numPartitions)
{
var list = new List<string>[numPartitions];
for (var i = 0; i < numPartitions; i++)
list[i] = new List<string>();
var index = 0;
foreach (var s in input)
list[(index = (index + 1) % numPartitions)].Add(s);
IList<IEnumerator<string>> result = new List<IEnumerator<string>>();
foreach (var l in list)
result.Add(l.GetEnumerator());
return result;
}
}
输出:
100
100
100
100
这显然有效。如果没有更多信息,就无法告诉您在特定实现中哪些方法无效。