What could cause “Destination array was not long enough” during List.Add in a single thread?

有些话、适合烂在心里 提交于 2021-02-10 09:22:51

问题


I have a List of objects that I'm adding to in nested foreach loops. the operation is synchronous (or maybe I don't understand lambdas as well as I think I do) and single-threaded and the list isn't unreasonably big. I'm at a total loss for what could be causing this exception.

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

Which results in:

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)

Trying to cover this with some unit tests and hammer on it, but I'm hoping someone can help me out in the mean time.

-EDIT-

Thanks to Juan and Mark I've figured out how this could happen. In my application, this operation is single threaded itself, but it uses what is essentially a singleton and is invoked via ajax. Multiple callers can start their own thread, and when those invocations are close enough together we get this behavior. I've made a console app to illustrate the concept.

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

The exception or a similar exception is thrown most of the time, but not every time. It's never thrown with the Fixed method.


回答1:


You are correct. The code that is manipulating the list does not use threading.

However, I think something is calling UpdateInstanceGraph repeatedly before the prior run has a chance to complete (and thus introducing threading). That would cause ElementInstances to be reset to size 0 while the prior call is still executing.

Change your code to use a local instance instead and then set the public property:

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

I would also recommend using SelectMany instead and casting to a List for direct assignment to the property:

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



回答2:


I think JuanR is close to right, but not exactly. This is surely a threading issue, and it surely originates outside the code you've posted. But it may or may not be concurrent calls to UpdateInstanceGraph, and if it is, they're both running the add method at the same time.[1]

The issue is going to be concurrent access to a single List object instance's methods. We know one of the threads is trying to add an element to the List ("innermost" statement of UpdateInstanceGraph). The other thread could be executing code from anywhere in the program, doing anything to the List instance, because you've provided a public getter for the list.

You could switch from List to a thread-safe implementation. I guess there's ArrayList from .NET 1.0 ; but MS docs indicate that this isn't very efficient compared to newer (.NET 4.0) thread-safe classes. Problem is, I can't seem to find a simple List type among the newer classes.

Another option would be to manage the concurrency everywhere that the app uses this List object, but that's seriously error prone. Yet another option is to write a thread-safe wrapper around List, but I can't see how that would be more efficient that using ArrayList.

Well, anyway, I know you say you've checked and re-checked that there's no concurrency issue, but if the app itself has any concept of concurrency, than given the public getter on the property for this List, I can't see how you could really know that; and the evidence strongly suggests it isn't so.


[1] The reason I say both threads would have to be running add at the same time is, if it were an issue of the "second" invocation "resetting the size to zero", that wouldn't have this symptom. The statement in question

ElementInstances = new List<ElementInstance>();

doesn't change a List object's size to 0; it rather creates a new List object (whose size is 0) and changes the reference of ElementInstances to this new instance. At the moment this happens, any add call that has already started in the "first" invocation would complete (successfully adding the element to a list that's no longer referenced); and any add call that hasn't already started will start against the new object (successfully adding the element to the new list... and eventually you'll notice that the prior elements are missing, but that's a totally different symptom).

It's concurrent access to the methods of a single instance of List that can lead to these kinds of weird exception.



来源:https://stackoverflow.com/questions/50240932/what-could-cause-destination-array-was-not-long-enough-during-list-add-in-a-si

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!