Parallel.ForEach 搜索未找到正确的值

Parallel.ForEach search doesn't find the correct value

这是我第一次尝试并行编程。

在我的真实应用程序中使用它之前,我正在编写一个测试控制台应用程序,但我似乎无法正确使用它。当我 运行 this 时,并行搜索总是比顺序搜索快,但并行搜索永远找不到正确的值。我做错了什么?

我在没有使用分区程序的情况下尝试过(只是Parallel.For);它比顺序循环慢并且给出了错误的数字。我看到一个 Microsoft 文档说对于简单的计算,使用 Partitioner.Create 可以加快速度。所以我试过了,但仍然得到了错误的值。然后我看到了Interlocked,但是我觉得我用错了

如有任何帮助,我们将不胜感激

Random r = new Random();
Stopwatch timer = new Stopwatch();

do {
    // Make and populate a list
    List<short> test = new List<short>();
    for (int x = 0; x <= 10000000; x++)
    {
        test.Add((short)(r.Next(short.MaxValue) * r.NextDouble()));
    }

    // Initialize result variables
    short rMin = short.MaxValue;
    short rMax = 0;

    // Do min/max normal search
    timer.Start();
    foreach (var amp in test)
    {
        rMin = Math.Min(rMin, amp);
        rMax = Math.Max(rMax, amp);
    }
    timer.Stop();

    // Display results
    Console.WriteLine($"rMin: {rMin}  rMax: {rMax}  Time: {timer.ElapsedMilliseconds}");

    // Initialize parallel result variables
    short pMin = short.MaxValue;
    short pMax = 0;

    // Create list partioner
    var rangePortioner = Partitioner.Create(0, test.Count);

    // Do min/max parallel search
    timer.Restart();
    Parallel.ForEach(rangePortioner, (range, loop) =>
    {
        short min = short.MaxValue;
        short max = 0;

        for (int i = range.Item1; i < range.Item2; i++)
        {
            min = Math.Min(min, test[i]);
            max = Math.Max(max, test[i]);
        }
        _ = Interlocked.Exchange(ref Unsafe.As<short, int>(ref pMin), Math.Min(pMin, min));
        _ = Interlocked.Exchange(ref Unsafe.As<short, int>(ref pMax), Math.Max(pMax, max));

    });
    timer.Stop();

    // Display results
    Console.WriteLine($"pMin: {pMin}  pMax: {pMax}  Time: {timer.ElapsedMilliseconds}");


    Console.WriteLine("Press enter to run again; any other key to quit");
} while (Console.ReadKey().Key == ConsoleKey.Enter);

示例输出:

rMin: 0  rMax: 32746  Time: 106
pMin: 0  pMax: 32679  Time: 66
Press enter to run again; any other key to quit

Interlocked.Exchange 仅对 Exchange 是线程安全的,每个 Math.MinMath.Max 都可以带有竞争条件。您应该分别为每个批次计算 min/max,然后合并结果。

使用像 Interlocked class 这样的低锁定技术是棘手且高级的。考虑到您在多线程方面的经验并不过分,我会说简单而可靠 lock:

object locker = new object();

//...

lock (locker)
{
    pMin = Math.Min(pMin, min);
    pMax = Math.Max(pMax, max);
}

像这样进行并行搜索的正确方法是为每个使用的线程计算本地值,然后在最后合并这些值。这确保仅在最后阶段需要同步:

var items = Enumerable.Range(0, 10000).ToList();

int globalMin = int.MaxValue;
int globalMax = int.MinValue;
Parallel.ForEach<int, (int Min, int Max)>(
    items, 
    () => (int.MaxValue, int.MinValue), // Create new min/max values for each thread used
    (item, state, localMinMax) =>
{
    var localMin = Math.Min(item, localMinMax.Min);
    var localMax = Math.Max(item, localMinMax.Max);
    return (localMin, localMax); // return the new min/max values for this thread
},
localMinMax => // called one last time for each thread used
{
    lock(items) // Since this may run concurrently, synchronization is needed
    {
        globalMin = Math.Min(globalMin, localMinMax.Min);
        globalMax = Math.Max(globalMax, localMinMax.Max);
    }
});

如您所见,这比常规循环要复杂得多,而且它甚至没有做任何像分区这样的花哨操作。优化的解决方案可以在更大的块上工作以减少开销,但为简单起见省略了这一点,看起来 OP 已经意识到这些问题。

请注意,多线程编程很困难。虽然在操场上而不是在真实的程序中尝试这些技术是个好主意,但我仍然建议您应该从研究线程安全的潜在危险开始,很容易找到这方面的好资源。

并不是所有的问题都会像这样明显错误,并且很容易导致百万分之一的问题,或者仅在 cpu 负载高时,或仅在单个 CPU 系统,或仅在代码投入生产后很久才检测到的问题。每当多个线程可能同时读写同一内存时,保持偏执是一个好习惯。

我还建议学习不可变数据类型和纯函数,因为一旦涉及多个线程,它们就更安全、更容易推理。