ParallelQuerys Count的数量是多少?

时间:2011-11-30 00:25:49

标签: c# .net linq parallel-processing

我正在测试自编写元素生成器(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;
    }

1 个答案:

答案 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()最有可能出现线程安全问题,因为它可能同时迭代多个枚举器,而所有其他解决方案将要求枚举器运行同步。如果没有看到您用于MyGeneratorMyPartitioner的代码,则难以确定线程安全问题是否可能是罪魁祸首。


为了演示,我编写了一个简单的枚举器,它将前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

这显然有效。如果没有更多信息,就无法告诉您在特定实现中哪些方法无效。