Hi.
I'm experiencing an interesting exception in my application. Unfortunately I do not have the opportunity to make a sample to recreate the problem right now. I'm first off looking for an answer to what is really going on, and how to solve. Or at least brainstorm a little.
Rough outline: Graphics are published to a GraphicsProvider to add/update graphics in the maps. GraphicsObservers listen, and add to GraphicsOverlays. This way all maps using the same GraphicsProvider will contain the same synchronized data. My MapGraphic hold some properties for routing for which GraphicsOverlay it belongs to.
Graphics are batch-published to the provider, but the internal routing will result in individual add operations to the GraphicsOverlay. In previous versions of the API, there was an AddRange method available, but I chose not to use that at the time to reduce complexity as well as hoping that the performance lost through individual add was gained from not having to filter out the destinations for AddRange to work. In 100.4, the AddRange is not available.
I have started multiple tasks to retrieve and publish data to the provider. If I execute one task at the time (more or less), everything is ok most of the time. On the other hand, if I execute multiple simultaneous tasks, I get the exception below. For this scenario, the MapView, map, layers, etc have already been loaded and contains and display data. There is only one map observing the data. I have also experienced the same exception if I only have one task.
My suspicion is that the simultaneous tasks experience conflicts with each other, and that perhaps RuntimeCollection.ExecuteActionWithUndo is unable to handle the load for some reason? All the operations in RuntimeCollection use lock(this.SyncRoot), so this is a bit beyond my understanding.
In the past I have experienced similar oddities, and in those cases it turned out it was related to exceptions occuring inside of the Queue class and it not being thread safe. Replacing with ConcurrentQue fixed our issue then. Could it be a similar situation going on with RuntimeCollection? While I can't really see how, considering SyncRoot is in use for all updates.
System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.
Parameter name: index
at Esri.ArcGISRuntime.RuntimeCollection`1.InsertItem(Int32 index, T item)
at Esri.ArcGISRuntime.RuntimeObservableCollection`1.InsertItem(Int32 index, T item)
at Shared.Map.MultiLayerGraphicsOverlayWrapper.Shared.Map.IGraphicsReceiver.OnAdd(MapGraphic graphic) in C:\Source\Shared.Map.Core\GroupLayer\MultiLayerGraphicsOverlayWrapper.cs:line 106
at Shared.Map.GroupLayerGraphicsObserver.AddToChildLayer(MapGraphic target, MapGraphic source) in C:\Source\Shared.Map.Core\GraphicsProviders\GroupLayerGraphicsObserver.cs:line 109
at Shared.Map.GroupLayerGraphicsObserver.OnPublish(MapGraphic source) in C:\Source\Shared.Map.Core\GraphicsProviders\GroupLayerGraphicsObserver.cs:line 78
at Shared.Map.GraphicsProvider.PublishInternal(MapGraphic graphic) in C:\Source\Shared.Map.Core\GraphicsProviders\GraphicsProvider.cs:line 62
at Shared.Map.GraphicsProvider.Publish(MapGraphic[] items) in C:\Source\Shared.Map.Core\GraphicsProviders\GraphicsProvider.cs:line 33
at Shared.Map.GraphicsProvider.Publish(IMapObject[] items) in C:\Source\Shared.Map.Core\GraphicsProviders\GraphicsProvider.cs:line 21
at Client.Resource.ResourceViewModel.<AddListInternal>z__OriginalMethod(IEnumerable`1 items) in C:\Source\Client\src\Resource\ResourceViewModel.cs:line 428
at Client.Resource.ResourceViewModel.<AddListInternal>c__Binding.Invoke(Object& instance, Arguments arguments, Object aspectArgs) in :line 16707564
at Client.Resource.ResourceViewModel.AddListInternal(IEnumerable`1 items) in :line 123
at Client.Resource.ResourceViewModel.InitializeFromServer(Int32[] districtIds) in C:\Source\Client\src\Resource\ResourceViewModel.cs:line 105
at Client.Resource.ResourceViewModel.Subscribed(Int32 districtId) in C:\Source\Client\src\Resource\ResourceViewModel.cs:line 127
at Client.District.DistrictViewModel.<>c__DisplayClass15_0.<UpdateNotifierSubscriptions>b__0() in C:\Source\Client\src\District\DistrictViewModel.cs:line 66
at System.Threading.Tasks.Task.Execute()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at Client.District.DistrictViewModel.<UpdateNotifierSubscriptions>d__15.MoveNext() in C:\Source\Client\src\District\DistrictViewModel.cs:line 59
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at Client.District.DistrictViewModel.<ToggleSelection>d__14.MoveNext() in C:\Source\Client\src\District\DistrictViewModel.cs:line 54
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at Client.District.DistrictViewModel.<OnUpdateRouting>d__13.MoveNext() in C:\Source\Client\src\District\DistrictViewModel.cs:line 48
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
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)
Solved! Go to Solution.
I'm looking at the code, and it looks like the range-check is done outside the lock (You could argue that's a bug). So to make the answer simple: RuntimeCollection is not a thread-safe collection (at least at this point), so don't update it from multiple threads at the same time.
Anyone from ESRI who have any thoughts on this? I've been too busy to create a repro sample, but I can add the details that if I add too many items at the same time, the issue appears to happen then too. I realize it might be tricky to give too much information without the ability to reproduce, but some pointers would be very helpful.
Do I understand it correctly that you are updating the same graphics collection from multiple threads at the same time?
Yes, that is correct, while "at the same time" depends on timing. Based on my findings today, I added 700 graphics from one thread, and still got the same exception.
I'm looking at the code, and it looks like the range-check is done outside the lock (You could argue that's a bug). So to make the answer simple: RuntimeCollection is not a thread-safe collection (at least at this point), so don't update it from multiple threads at the same time.
Oh, yeah. seems I missed that. That's embarrassing Overworked brains don't work too well.
Should be a quick fix for the next version? I will see what I can do to either synchronize on my own end, or change threading until then. Thank you so much.
Hi,
I can see this issue has not been addressed for 100.6, so I was just wondering if it is on the shortlist for the next version?
If you don't need to insert graphics at a specific position, prefer to use the Add method -- it appends to the end of the collection and doesn't require an index. You will still need locking if you need to guarantee adding all items in one continuous block, to guard against concurrent modification.
Alternatively, you can use the new AddRange method added in Runtime 100.6.0. It does not require additional locking and always appends the new items in one continuous block. It's actually a little faster too.
RuntimeCollection's Insert method is only thread-safe in a sense that it will not corrupt internal state of the collection. However if you are inserting multiple items in a loop -- or if you are relying on collection's Count to calculate the insertion index -- then you need to guard against concurrent modification. For example, you can lock on its SyncRoot until you are done adding the whole batch of Graphics:
var graphicsCollection = yourOverlay.Graphics;
lock (((ICollection)graphicsCollection).SyncRoot)
{
for (int i=0; i<999; i++){
int myIndex = graphicsCollection.Count/2 + i; // as an example
graphicsCollection.Insert(myIndex, new Graphic());
}
}
Thanks. I've already worked around the issue with my own locking, but I was hoping to reduce some additional locking and complexity in my own code.
But I'm glad to hear the AddRange method is back