在单个线程中的List.Add期间,什么可能导致“目标数组不够长”?

时间:2018-05-08 19:23:34

标签: c# arrays list

我在嵌套的foreach循环中添加了一个对象列表。操作是同步的(或者我不理解lambdas以及我认为我做的)和单线程,并且列表并不是不合理的大。因为导致这种异常的原因,我完全失去了。

public string PromotionSpecificationIdGuid { get; set; }
public virtual List<ElementInstance> ElementInstances { get; set; }

public void UpdateInstanceGraph(OfferingInstance parentData, OfferingInstance offeringContainer = null)
{
    ElementInstances = new List<ElementInstance>();

    parentData.ActiveServices.ForEach(
        service => service.ActiveComponents.ForEach(
            component => component.Elements.ForEach(
                element =>
                {
                    if (element.PromotionId == this.PromotionSpecificationIdGuid)
                    {
                        ElementInstances.Add(element);
                    }
                })));
}

结果是:

System.ArgumentException: Destination array was not long enough. Check destIndex and length, and the array's lower bounds.
       at System.Array.Copy(Array sourceArray, Int32 sourceIndex, Array destinationArray, Int32 destinationIndex, Int32 length, Boolean reliable)
       at System.Collections.Generic.List`1.set_Capacity(Int32 value)
       at System.Collections.Generic.List`1.EnsureCapacity(Int32 min)
       at System.Collections.Generic.List`1.Add(T item)

尝试通过一些单元测试来解决这个问题并对其进行锤击,但我希望有人可以帮助我。

- 编辑 -

感谢Juan和Mark我已经弄清楚这是怎么发生的。在我的应用程序中,此操作本身是单线程的,但它使用基本上是单例并通过ajax调用。多个调用者可以启动自己的线程,当这些调用足够接近时,我们会得到这种行为。我已经制作了一个控制台应用来说明这个概念。

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

namespace listaccessmonster
{
    public class Program
    {
        private static List<Guid> baseList = new List<Guid>();
        private static List<Guid> activeList;
        private static Random rand = new Random();

        public static void Main(string[] args)
        {
            for(int i = 0; i < 1000000; i++)
            {
                baseList.Add(Guid.NewGuid());
            }

            var task1 = UpdateList(); //represents ajax call 1
            var task2 = UpdateList(); //represents ajax call 2

            var result = Task.WhenAll(task1, task2);

            try
            {
                result.Wait();
            }
            catch(Exception e)
            {
                Console.WriteLine(e);
            }

            task1 = UpdateListFixed(); //represents ajax call 1
            task2 = UpdateListFixed(); //represents ajax call 2

            result = Task.WhenAll(task1, task2);

            try
            {
                result.Wait();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }

            Console.WriteLine("press Enter to exit");
            Console.ReadKey();
        }

        private static Task UpdateList()
        {
            return Task.Run(()=> {
                Thread.Sleep(rand.Next(5));
                Console.WriteLine("Beginning UpdateList");
                activeList = new List<Guid>();
                baseList.ForEach(x => {
                    activeList.Add(x);
                });
            });
        }

        private static Task UpdateListFixed()
        {
            return Task.Run(() => {
                Thread.Sleep(rand.Next(5));
                Console.WriteLine("Beginning UpdateListFixed");
                var tempList = new List<Guid>();
                baseList.ForEach(x => {
                    tempList.Add(x);
                });
                activeList = tempList;
            });
        }
    }
}

大多数情况下会抛出异常或类似的异常,但不是每次都抛出异常。使用Fixed方法永远不会抛出它。

2 个答案:

答案 0 :(得分:2)

你是对的。操作列表的代码不使用线程。

但是,我认为在先前的运行有机会完成之前反复调用UpdateInstanceGraph(从而引入线程)。这会导致ElementInstances在前一次调用仍在执行时重置为0

将代码更改为使用本地实例,然后设置公共属性:

public void UpdateInstanceGraph(OfferingInstance parentData, OfferingInstance offeringContainer = null)
{
    var instances = new List<ElementInstance>(); 

    parentData.ActiveServices.ForEach(
        service => service.ActiveComponents.ForEach(
            component => component.Elements.ForEach(
                element =>
                {
                    if (element.PromotionId == this.PromotionSpecificationIdGuid)
                    {
                        instances.Add(element);
                    }
                })));
    ElementInstances = instances;
}

我还建议您使用SelectMany代替并投射到List以直接分配给该媒体资源:

public void UpdateInstanceGraph(OfferingInstance parentData, OfferingInstance offeringContainer = null)
{

    ElementInstances  = parentData.ActiveServices
        .SelectMany(s => s.ActiveComponents)
        .SelectMany(c => c.Elements)
        .Where(e => e.PromotionId == PromotionSpecificationIdGuid).ToList();
}

答案 1 :(得分:2)

我认为JuanR接近正确,但不完全正确。这肯定是一个线程问题,它肯定来自您发布的代码之外。但它可能是也可能不是UpdateInstanceGraph的并发调用,如果是,它们同时运行add方法。[1]

问题是对单个List对象实例的方法进行并发访问。我们知道其中一个主题是尝试add ListUpdateInstanceGraph的“最内层”语句)的元素。另一个线程可以从程序中的任何位置执行代码,对List实例执行任何操作,因为您已为列表提供了公共getter。

您可以从List切换到线程安全的实现。我猜.NET 1.0中有ArrayList;但MS文档表明,与较新的(.NET 4.0)线程安全类相比,这不是非常有效。问题是,我似乎无法在较新的类中找到简单的List类型。

另一种选择是在应用程序使用此List对象的任何地方管理并发性,但这很容易出错。另一种选择是围绕List编写一个线程安全的包装器,但是我看不出使用ArrayList会更有效。

好吧,无论如何,我知道你说你已经检查并重新检查了没有并发问题,但如果应用程序本身有任何并发​​概念,那么给定该属性的公共getter,我可以不知道你怎么知道那个;并且有证据表明事实并非如此。

[1]我说两个线程必须同时运行add的原因是,如果它是“第二次”调用“将大小重置为零”的问题,那就不会有这种症状。有问题的陈述

ElementInstances = new List<ElementInstance>();

不会将List对象的大小更改为0;它创建一个新的List对象(其大小为0),并将ElementInstances引用更改为此新实例。在发生这种情况时,已经在“第一次”调用中启动的任何add调用都将完成(将该元素成功添加到不再引用的列表中);并且任何尚未启动的add调用将从新对象开始(成功将元素添加到新列表中......最终您会注意到先前的元素丢失,但这是完全不同的症状)。

它可以同时访问单个List实例的方法,这些方法可能导致这些奇怪的异常。