并行嵌套操作 return 奇怪的结果

Parallel nested operations return weird results

我正在尝试为我的代码使用并行库,但我遇到了一个奇怪的问题。 我做了一个简短的程序来演示这种行为。简而言之,我做了 2 个循环(一个在另一个循环中)。第一个循环生成一个包含 200 个整数的随机数组,第二个循环将所有数组添加到一个大列表中。 问题是,最后,我没有得到 200 整数的倍数,而是我看到一些运行没有等待随机数组完全加载。 很难解释,所以这里是示例代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace TestParallel
{
    class Program
    {
        static int RecommendedDegreesOfParallelism = 8;
        static int DefaultMaxPageSize = 200;

        static void Main(string[] args)
        {
            int maxPage = 50;
            List<int> lstData = new List<int>();
            Parallel.For(0, RecommendedDegreesOfParallelism, new ParallelOptions() { MaxDegreeOfParallelism = RecommendedDegreesOfParallelism },
                (index) =>
                {
                    int cptItems = 0;
                    int cptPage = 1 - RecommendedDegreesOfParallelism + index;
                    int idx = index;
                    do
                    {
                        cptPage += RecommendedDegreesOfParallelism;
                        if (cptPage > maxPage) break;

                        int Min = 0;
                        int Max = 20;
                        Random randNum = new Random();
                        int[] test2 = Enumerable
                            .Repeat(0, DefaultMaxPageSize)
                            .Select(i => randNum.Next(Min, Max))
                            .ToArray();
                        var lstItems = new List<int>();
                        lstItems.AddRange(test2);
                        var lstRes = new List<int>();
                        lstItems.AsParallel().WithDegreeOfParallelism(8).ForAll((item) =>
                        {
                            lstRes.Add(item);
                        });

                        Console.WriteLine($"{Task.CurrentId} = {lstRes.Count}");
                        lstData.AddRange(lstRes);
                        cptItems = lstRes.Count;
                    } while (cptItems == DefaultMaxPageSize);
                }
            );
            Console.WriteLine($"END: {lstData.Count}");
            Console.ReadKey();
        }
    }
}

这是执行日志:

4 = 200
1 = 200
2 = 200
3 = 200
6 = 200
5 = 200
7 = 200
8 = 200
1 = 200
6 = 194
2 = 191
5 = 200
7 = 200
8 = 200
4 = 200
5 = 200
3 = 182
4 = 176
8 = 150
7 = 200
5 = 147
1 = 200
7 = 189
1 = 200
1 = 198
END: 4827

我们可以看到一些循环 return 少于 200 项。 怎么可能?

这不是线程安全的:

lstItems.AsParallel().WithDegreeOfParallelism(8).ForAll((item) =>
{
    lstRes.Add(item);
});

来自 List<T> 的文档:

It is safe to perform multiple read operations on a List, but issues can occur if the collection is modified while it's being read. To ensure thread safety, lock the collection during a read or write operation. To enable a collection to be accessed by multiple threads for reading and writing, you must implement your own synchronization.

没有明确提及,但.Add()在多个线程同时调用时也会失败。

解决方案是在上面的循环中锁定对 List<T>.Add() 的调用,但如果这样做,它可能会比在单个线程中的循环中添加项目慢。

var locker = new object();

lstItems.AsParallel().WithDegreeOfParallelism(8).ForAll((item) =>
{
    lock (locker)
    {
         lstRes.Add(item);
    }
});