Parallel.ForEach 循环的执行方式类似于串行循环

Parallel.ForEach loop is performing like a serial loop

我已经花了大约 8 个多小时在线搜索帮助,但我找不到任何东西,所以,开始吧。

我正在使用 Team Foundation Server 和 C#,我正在尝试获取工作项列表并将它们转换为我们为绑定到特殊 UI 而创建的通用对象。要处理的项目是特定日期的任务,列表大约有 30 个左右的项目,所以没什么大不了的。

循环看起来像这样:

List<IWorkItemData> workitems = new List<IWorkItemData>();
var queryForData = Store.Query(query).Cast<WorkItem>();

if (queryForData.Count() == 0)
    return workitems;

Parallel.ForEach(queryForData, (wi) =>
{
    var temp = wi;
    lock (workitems)
    {
        TFSWorkItemData tfsWorkItem = new TFSWorkItemData(temp);
        workitems.Add(tfsWorkItem);
    }
});

TFSWorkItemData 的构造函数内部如下所示:

public TFSWorkItemData(WorkItem workItem)
{
    this.workItem = workItem;

    this.Fields = new Dictionary<string, IFieldData>();

    // Add Fields
    foreach (Field field in workItem.Fields)
    {
        TFSFieldData fieldData = new TFSFieldData
        {
            Value = field.Value,
            OldValue = field.OriginalValue,
            ReferenceName = field.ReferenceName,
            FriendlyName = field.Name,
            ValueType = field.FieldDefinition.SystemType
        };
        this.Fields.Add(field.ReferenceName, fieldData);
    }
}

因此执行此操作大约需要 90 秒。我知道抓取 30 个工作项目不需要那么长时间,所以一定是我正在做的事情导致这需要这么长时间。我知道锁会影响性能,但是当我删除它时,我得到一个 InvalidOperationException 说集合已被修改。当我查看此异常的详细信息时,我能找到的唯一有用信息是该集合是一本字典。奇怪的是,在我看来,工作项中的字段字典根本没有被修改。我的 class 中的字典只是被添加到,除非我遗漏了什么,否则这不应该是罪魁祸首。

请帮我找出我在字典方面做错了什么。我确实尝试将并行 foreach 循环移动到 workitem.Fields 集合,但我似乎无法让它工作。

编辑:阅读 Answer 的评论以获得此问题的答案。谢谢。

Please help me figure out what I'm doing wrong regarding the dictionaries.

抛出异常是因为List<T>不是线程安全的。

您有一个需要修改的共享资源,使用 Parallel.ForEach 并没有多大帮助,因为您正在将瓶颈移至 lock,导致那里的争用,即可能是为什么您看到性能实际上 下降 。线程不是一个神奇的解决方案。你需要渴望有尽可能多的独立工作者,每个人都可以做自己的工作。

相反,您可以尝试使用 PLINQ,这将在内部对您的枚举进行分区。由于您实际上想要投影集合中的每个元素,因此可以使用 Enumerable.Select:

 var workItems = queryForData.AsParallel().Select(workItem => new TFSWorkItemData(workItem)).ToArray();

为了确定此解决方案是否真的比顺序迭代更好,请对您的代码进行基准测试。永远不要假设线程越多速度越快。

我发现了另一种可能适用于尝试做类似事情的人的方法。

// collect all of the IDs
var itemIDs = Store.Query(query).Cast<WorkItem>().Select(wi = wi.Id).ToArray();

IWorkItemData[] workitems = new IWorkItemData[itemIDs.Length];

// then go through the list, get the complete workitem, create the wrapper,
// and finally add it to the collection
System.Threading.Tasks.Parallel.For(0, itemIDs.Length, i =>
{
    var workItem = Store.GetWorkItem(itemIDs[i]);
    var item = new TFSWorkItemData(workItem);
    workitems[i] = item;
});

编辑:将列表更改为数组