【问题标题】:Parallel.ForEach search doesn't find the correct valueParallel.ForEach 搜索找不到正确的值
【发布时间】:2021-11-12 18:43:24
【问题描述】:

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

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

我在没有使用分区器的情况下尝试过(只是Parallel.For);它比顺序循环慢并且给出了错误的数字。我看到一个微软文档说对于简单的计算,使用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

【问题讨论】:

    标签: c# parallel-processing thread-safety race-condition parallel.foreach


    【解决方案1】:

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

    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 系统上,或仅在在代码投入生产后很久才检测到。当多个线程可能同时读取和写入相同的内存时,保持偏执是一个好习惯。

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

    【讨论】:

    • 谢谢!我会调查你提到的。该代码运行良好,给出了正确的值,并且始终快约 3 倍
    • 我几乎有过一次。我只是犯了一个错误,试图将(min, max} 传递给 ForEach 主体 lambda 函数而不是 localMinMax。 :face-palm:
    • 这是一个很好的解决方案 (+1)。我希望使用Partitioner 创建范围应该更有效,因为它消除了为每个元素调用 lambda 的开销(lambdas are not inlinable)。而且这个特定的并行循环的计算是如此的轻量级,以至于这个开销可能是不可忽略的。
    【解决方案2】:

    Interlocked.Exchange 仅对 Exchange 是线程安全的,每个 Math.MinMath.Max 都可以具有竞争条件。您应该分别计算每个批次的最小值/最大值,然后加入结果。

    【讨论】:

    • 我认为这就是我在 parallel.ForEach 中使用 min/max 来查找块 min/max 然后进行交换的方法。还是您的意思是列出块最小/最大值的列表,然后在 Parallel.ForEach 之外的列表中列出最终的最小值/最大值?
    • 我试过了,现在我得到了正确的值。我注意到的一件有趣的事情是,第一次通过 main do while 循环时,两个最小/最大循环的执行时间大致相同,但在随后的循环中,并行速度大约快 4 倍。知道这是为什么吗?大多数情况下,我只需要为每个数据集运行一次最小值/最大值。有什么我可以改变的,让它一直更快,或者我不应该为这个任务而烦恼并行。我只是想尝试一下,因为我要搜索多达 3000 万个数据点。
    • @master_ruko JITter 必须在运行之前将 MSIL 编译为本机代码,因此预计在您第一次调用像 TPL 这样的大块库的方法时,您会受到性能影响。 link, link, link
    • @TheodorZoulias 谢谢,我没想到。
    【解决方案3】:

    使用像Interlocked 类这样的低锁定技术是复杂而先进的。考虑到你在多线程方面的经验并不过分,我想说一个简单可靠的lock

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

    【讨论】:

    • 我试过了,我得到了正确的值,但现在并行比顺序慢。
    • 我没有测试过,但应该不会慢。锁保护块应该在每个范围内只执行一次,并且分区器应该只创建少数范围。
    猜你喜欢
    • 2023-03-14
    • 2018-07-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多