-1

I have a class setup as shown - I understand a longrunning task would be better, but this is the original state in a very trimmed down fasion. I have trimmed out anything that doesn't seem necessary to the problem.

TLDR:

  1. Code runs in a loop and the listed conditions (1/2/3) can be executed independently
  2. Logs show that condition3 only appears to execute when currentParentRun.ChildRuns.Count is > 0
  3. The error is thrown and caught indicating (along with other logs) that in the BeginInvoke's called function, that currentParentRun.ChildRuns[currentParentRun.ChildRuns.Count - 1] with a >0 count is somehow out of range.

    // Class created and owned by UI
    public class DataProcessor
    {
        private BackgroundWorker worker = new BackgroundWorker();

        private ParentRun currentParentRun = null;

        public DataProcessor()
        {
            // register background worker events
            worker.WorkerSupportsCancellation = false;
            worker.WorkerReportsProgress = false;
            worker.DoWork += worker_DoWork;
            worker.RunWorkerCompleted += worker_RunWorkerCompleted;

            worker.RunWorkerAsync();
        }

        private void worker_DoWork(object sender, DoWorkEventArgs e)
        {
            while (process)
            {
                try
                {
                    // Create a new parent
                    if (condition1)
                    {
                        currentParentRun = new ParentRun();
                    }

                    // Create a new child (parent will always be not null here)
                    if (condition2)
                    {
                        ChildRun childRun = new ChildRun();

                        currentParentRun.ChildRuns.Add(childRun);
                    }
                    
                    // Call the UI and update
                    if (condition3)
                    {
                        ShowFinishedChildUI();
                    }
                    
                    System.Threading.Thread.Sleep(loopDelayProcessor);
                }
                catch (Exception ex)
                {
                }
            }
        }
        
        public void ShowFinishedChildUI()
        {
            System.Windows.Application.Current.Dispatcher.BeginInvoke((Action)(() => ShowFinishedChildUIDelegate()));
        }

        public void ShowFinishedChildUIDelegate()
        {
            // Parent is the UI being updated
            parent.ShowFinishedChild(currentParentRun.ParentRunID, 
                currentParentRun.ChildRuns[currentParentRun.ChildRuns.Count - 1].ChildRunID);

            resetParentDisplay = false;
        }
    }

Here, "parent" is the owning UI control and we use BeginInvoke to send call a UI function to update.

What's occuring every 20-30k new "parent" creations is occasionally, while in the middle of processing, there is an error in the ShowFinishedChildUIDelegate function of Index was out of range. Must be non-negative and less than the size of the collection. As far as I can verify, it is always at least non-zero, which leads me to think that somewhere between in the same line of code, the number of children changes between

currentParentRun.ChildRuns[...]

and

currentParentRun.ChildRuns.Count - 1

Is it possible that if condition2 in the main code block is fulfilled while the delegate is processing, that the same line of code can have different values, where the count of the children is greater than the number of classes in the list? That's my best guess, but I'm not sure how I can verify that.

Thanks.

More detailed error:

2021-03-19 12:00:12.8174|ERROR|GeneralLogger|UnhandledException StackTrace :    at System.ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource)
   at System.Collections.Generic.List`1.get_Item(Int32 index)
   at QASystem.Classes.DataProcessor.ShowFinishedChildUIDelegate() in D:\..\QASystem\QASystem\Classes\DataProcessor.cs:line 756
   at QASystem.Classes.DataProcessor.<ShowFinishedChildUI>b__76_0() in D:\..\QASystem\QASystem\Classes\DataProcessor.cs:line 751
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.DispatcherOperation.InvokeImpl()
   at System.Windows.Threading.DispatcherOperation.InvokeInSecurityContext(Object state)
   at MS.Internal.CulturePreservingExecutionContext.CallbackWrapper(Object obj)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at MS.Internal.CulturePreservingExecutionContext.Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Windows.Threading.DispatcherOperation.Invoke()
   at System.Windows.Threading.Dispatcher.ProcessQueue()
   at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
   at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
   at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
   at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
   at System.Windows.Application.RunDispatcher(Object ignore)
   at System.Windows.Application.RunInternal(Window window)
   at System.Windows.Application.Run(Window window)
   at System.Windows.Application.Run()
   at QASystem.App.Main()
James F
  • 535
  • 9
  • 26
  • Tag your GUI: WPF, WinForms, etc? – LarsTech Mar 22 '21 at 14:46
  • 1
    Avoid empty Try-Catches. It's hiding problems from you. Without know what "condition#" means, your code might be making `condition2` false but `condition3` true, so then it could throw that exception. But we don't see that code. – LarsTech Mar 22 '21 at 15:12
  • I trimmed out the try catch to make the code succinct, it's what logs the out of range error. `Condition3` could execute without `condition2` - that's valid - the issue is that this (`currentParentRun.ChildRuns[currentParentRun.ChildRuns.Count - 1]`) can give me an index out of range exception when `.Count` is > 0, which seems impossible. – James F Mar 22 '21 at 15:15
  • Added a more detailed error – James F Mar 22 '21 at 15:21
  • 1
    As a side note the `BackgroundWorker` class has the events `ProgressChanged` and `RunWorkerCompleted` that you are supposed to use in order to interact with the UI during and after the processing. Using the `Dispatcher` to communicate with the UI negates any advantage of using a `BackgroundWorker` in the first place. You could start a `Thread` manually instead, and it wouldn't make any difference. Btw the `BackgroundWorker` is [technologically obsolete](https://stackoverflow.com/questions/12414601/async-await-vs-backgroundworker/64620920#64620920) IMHO. Async/await has made it irrelevant. – Theodor Zoulias Mar 22 '21 at 15:26
  • 1
    Thanks, good to know! Yeah, it's one of the long term goals to switch out of `BackgroundWorker` - this is inherited and it's a slow grind to a new base :) – James F Mar 22 '21 at 15:29
  • `Condition3 could execute without condition2` Well, there it is. Your posted code is not checking if `.Count is > 0` – LarsTech Mar 22 '21 at 15:31
  • In trying to keep the code clean, this code is missing thousands of lines of actual functional code, including the indication that `condition3` logging at time of this error indicates the child count is greater than 0, both in replay of raw data processed and logging of record counts. I mentioned it in the original question, but it may not have stood out - it appears that in one line of code, the actual ARRAY elements is different that ARRAY.Count, but I don't know how that could be. – James F Mar 22 '21 at 15:35

1 Answers1

2

A likely guess is that the code is not thread safe. The example contains no synchronization at all. For example, the code could be run in the following order:

  1. Thread A: var count = currentParentRun.ChildRuns.Count - 1;
  2. Thread B: currentParentRun = new ParentRun();
  3. Thread A: currentParentRun.ChildRuns[count];

As you can see this can result in incorrect, thread-unsafe behavior.

One way to solve this would be to insert locks to prevent modification while the UI is updated. Another way would be to create a copy of the data and give this copy to the UI, thus preventing any risk that the same data is accessed by multiple threads. I.e.

 var parentId = currentParentRun.ParentRunID; 
 var childId = currentParentRun.ChildRuns[currentParentRun.ChildRuns.Count - 1].ChildRunID;
 Dispatcher.BeginInvoke((Action)(() => ShowFinishedChildUIDelegate(parentId, childId)));
JonasH
  • 28,608
  • 2
  • 10
  • 23
  • Thanks! Your code block is something I've started putting into place actually and I'm glad that stands out to you. It definitely seems a thread issue, but I've never seen anything like `currentParentRun.ChildRuns[currentParentRun.ChildRuns.Count - 1]` being an issue with > 0 elements. I will investigate better threadsafe access as well. – James F Mar 22 '21 at 15:41