Parallel.For有不寻常的行为

时间:2013-01-31 21:01:56

标签: c# math for-loop parallel-processing

我正在尝试将此主要筛选功能转移到使用Parallel.For,因此它可以使用多个核心。

然而,当我运行它时,b变量的值似乎随机跳跃甚至根本不会改变,尤其是对于更高的To值。

static List<long> Sieve(long To)
{
    long f = 0;
    To /= 2;

    List<long> Primes = new List<long>();
    bool[] Trues = new bool[To];

    if (To > 0)
        Primes.Add(2);

    long b = 0;

    Parallel.For(1L, To, a =>
    {
        b++;

        if (Trues[b])
            return;

        f = 2 * b + 1;
        Primes.Add(f);

        for (long j = f + b; j < To; j += f)
            Trues[j] = true;
    });

    return Primes;
}

发生了什么,我怎么能阻止这种情况发生?

3 个答案:

答案 0 :(得分:2)

这里面临的问题叫做race conditions,当多个CPU核心将相同的变量加载到各自的缓存中,然后处理它,然后将值写回RAM时,会发生什么。显然,写回RAM的值可能在此期间已经过时(例如,当核心在用另一个值覆盖之前加载变量时)

首先:我不会使用b++而是使用int i = Interlocked.Increment(ref b);。 Interlocked.Increment确保没有2个线程尝试同时递增相同的值。结果是递增的值将保存到变量i中。这是非常重要,因为您需要该值在for循环的每次迭代中保持不变,否则这是不可能的,因为其他线程将递增此变量。

接下来是变量fa(定义为For-iterator)。忘记f,请改用a

f = 2 * b + 1; // wrong
a = 2 * b + 1; // correct

最后:System.Collections.Generic.List是 NOT ,我重复(因为它很重要) NOT 线程安全。有关详细信息,请参阅http://msdn.microsoft.com/en-us/library/6sh2ey19.aspx

Primes.Add(f); // will likely break something
lock (Primes)  // LOCK the list
{
    Primes.Add(a); // don't forget, we're using 'a' instead of 'f' now
}

lock关键字仅接受引用类型变量作为参数,这是因为锁定变量 NOT 会阻止另一个线程访问它。相反,您可以将其想象为在引用顶部设置标志,以便发出其他线程的信号I'm working here, please do not disturb!

当然,如果另一个线程试图访问Primes而没有要求事先锁定它,那么该线程仍然可以访问该变量。

你应该已经学会了所有这些,因为Parallel Prime Sieve是第一次学习多线程时最常见的初学练习之一。

修改

完成上述所有步骤后,程序不应该运行; 然而这并不意味着解决方案是正确的,或者您已经获得了加速,因为您的许多线程将会重复工作。

假设Thread a有责任标记3的每个倍数,而Thread n则负责标记9的倍数。当按顺序运行时,Thread n开始处理9的倍数,它将看到9已经是另一个(素数)的倍数。但是,由于您的代码现在是并行的,因此无保证将在时间Thread n开始工作时标记9。更不用说 - 因为9可能没有标记 - 可能会被添加到素数列表中。

因此,您必须顺序查找1和To的平方根之间的所有素数。为什么To的平方根?这是你必须自己找到的东西。

一旦找到从1到To的平方根的所有素数,就可以使用之前找到的所有素数来启动平行素数筛,以找到其余的素数。

最后一个值得注意的一点是,Primes只应在 Trues填充后才构建。那是因为:

1。您的线程只需将2的倍数处理为To的平方根,因此在当前实现中不会再向{{1}添加任何元素超越根。

2。如果您选择让线程超越root,那么您将遇到问题,您的某个线程将很快向Primes添加非素数在另一个线程将该数字标记为非素数之前,这不是您想要的。

3。即使您很幸运且Primes的所有元素确实都是1到Primes之间的所有素数,它们可能不一定是有序的,要求To先排序。

答案 1 :(得分:1)

b跨线程共享。如果多个线程同时敲响那个糟糕的变量,那么你会发生什么呢?

似乎ba在您的代码中总是相等(或者相差一个)。使用a。并同步访问所有其他共享状态(如列表)。

答案 2 :(得分:1)

欢迎来到精彩的多线程世界。

马上,我可以看到循环的每次迭代都会b++,然后在整个过程中使用b。这意味着循环的每次迭代都将在所有其他迭代中修改b的值。

您可能想要做的是使用内联函数中提供的a变量,这与您在b中尝试的完全相同。如果不是这种情况,那么你应该研究锁定b并将其值复制到本地(每个迭代)变量,然后再对其进行处理。

试试这个,让我知道你想做什么:

static List<long> Sieve(long To)
{
    To /= 2;

    List<long> Primes = new List<long>();

    if (To > 0)
        Primes.Add(2);

    Parallel.For(1L, To, a =>
    {
        long f = 2 * a + 1;
        Primes.Add(f);
    });

    Primes.Sort();

    return Primes;
}