Thread access control on resource after Dispatcher.Invoke not working

69 views Asked by At

I am struggling with an issue related to preventing resource access via the same thread (UI thread). Here is the scenario. The user clicks an object on the screen. Based on the selected object, a plot is drawn (and a member variable of the class is set to the selected object). Updates to the position of that selected object are received via a CollectionChanged event on that object. That handler uses Dispatcher.Invoke to marshall to the UI thread where additional updates to the plot take place. At any time, the user can "de-select" the object. When that happens, the class' member variable for the selected item is set to null. The problem is, an update can be received at any time. When this happens, it attempts to access data on the now null member variable. I have tried Lock, mutex, semaphore, SemiphoreSlim with ctor values of 1,1, etc.. However nothing seems to block the UI thread from attempting to access the member variable in both the event handler for the collection changed event and the user selection event handler (via a dependency object value changed event) from accessing the member variable at the same time.

DependencyObject value changed event handler (unrelated code removed):

            try
            {
                mSemaphore.Wait();
                if (mTrackHistory != null)
                {
                    mTrackHistory.PositionData.CollectionChanged -= 
                    PositionData_CollectionChanged;
                }

                mTrackHistory = e.NewValue as TrackHistoryInfo;

                // If track history is null, don't do anything....
                if (mTrackHistory != null)
                {
                    Create initial plot
                }
            }
            finally
            {
                mSemaphore.Release();
            }
        }

And the CollectionChanged event handler:

if (mDispatcher != Dispatcher.CurrentDispatcher)
            {
                mDispatcher.Invoke(() => PositionData_CollectionChanged(sender, e));
            }
            else
            {
                try
                {
                    mSemaphore.Wait();
                    if (e.Action == 
                    System.Collections.Specialized.NotifyCollectionChangedAction.Add)
                    {
                        // Update the plot...
                        for (int idx = 0; idx < e.NewItems.Count; idx++)
                        {
                            (ECEF Position, DateTime Time) newInfo = ((ECEF Position, DateTime 
                            Time))e.NewItems[idx];
                            
                            // This is where it blows up attempting to access a null mTrackHistory
                            TimeSpan ts = newInfo.Time - mTrackHistory.StandupTime;
                            string stamp = $"{ts.Hours:D2}:{ts.Minutes:D2}:{ts.Seconds:D2}";

                           // Update plot with latest data
                       }
                            
                }
                finally
                {
                    mSemaphore.Release();
                }

Any suggestions/direction on how to prevent this issue is greatly appreciated!

1

There are 1 answers

3
JonasH On

I would recommend something like the following pattern when doing any kind of background calculation:

public void OnObjectSelected(){
    var localSelectedObject = SelectedObjectProperty;
    if(localSelectedObject == null){
        ResetPlot();
        return
    }

    var result = await Task.Run(() => DoWorkOnBackgroundThread(localSelectedObject));

    // The selected object could have changed while the background thread was working
    // So check if it is still the right data to update
    if(localSelectedObject == SelectedObjectProperty){
        UpdatePlot(localSelectedObject);
    }
}

This pattern removes any need for Dispatch.Invoke since everything except DoWorkOnBackgroundThread runs on the UI thread. You also do not have to worry about the selected object being set to null, since the background thread uses a local copy that is guaranteed to never change.

You may also want to add functionality for cancelling the background work if the selected object changes, since the result will no longer be needed.