【问题标题】:Parallel.ForEach loop is performing like a serial loopParallel.ForEach 循环的执行类似于串行循环
【发布时间】:2015-06-18 22:14:57
【问题描述】:

我花了大约 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 说集合已被修改。当我查看此异常的详细信息时,我能找到的唯一有用信息是该集合是一本字典。奇怪的是,在我看来,工作项中的字段字典根本没有被修改。而且我班上的字典只是被添加了,除非我遗漏了什么,否则这不应该是罪魁祸首。

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

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

【问题讨论】:

  • 您正在锁定列表。将项目添加到列表是并行任务所做的唯一事情。但是您正在锁定列表。 当然它会像连续剧一样。
  • 还有,你不是说new TFSWorkItemData (temp)吗?
  • 锁定列表是唯一可以防止我得到的 InvalidOperationException 的方法。感谢您的快速响应并指出错误。
  • 请记住,Parallel.ForEach 不提供任何关于并行化程度的保证。

标签: c# tfs parallel-processing invalidoperationexception parallel.foreach


【解决方案1】:

请帮我弄清楚我在字典方面做错了什么。

抛出异常是因为List&lt;T&gt; 不是线程安全的。

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

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

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

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

【讨论】:

  • 这要好得多,但仍然可能不会提高性能。我怀疑TFSWorkItemData 构造函数中是否真的发生了那么多事情,除非访问字段是从 TFS 读取的。
  • @JohnSaunders 我同意,这就是为什么我说基准测试将是唯一的真理。如果这是一个简单的小迭代,那也无济于事。
  • 非常感谢,我会试试的。
  • 看起来我得到了同样的 InvalidOperationException 所以也许 John 是对的并且访问工作项中的字段是从 TFS 读取的。不过,我很欣赏更好的解决方案。
  • 好吧,我认为约翰是对的。根据this site,在表的“选择”部分中,它说如果正在访问的字段不在查询中(其中许多不在),则需要再次往返数据库。所以这就是我认为修改收藏的原因。谢谢你们的帮助。
【解决方案2】:

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

// 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;
});

编辑:将列表更改为数组

【讨论】:

  • 您的“提供的解决方案”不是线程安全的。您正在并行附加到 List&lt;T&gt;
  • 你说得对,它不是。希望我最近的编辑能解决问题。这就是我现在在代码中的做法。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-23
  • 2012-01-03
相关资源
最近更新 更多