phil13131 February 2016

BeginInvoke causes error due to skipped null check

I encountered a to me not understandable error caused by using Dispatcher.BeginInvoke within a multi-threaded application.

My program contains a List of objects through which I loop using multiple threads and perform some calculations. I have simplified (and slightly modified) my code structure to the very bare essentials, so it will hopefully be easier to understand:

public class Foo
{
    public void DoCalc()
    {
        //do calculations
    }
}

public class Foo2
{
    public Foo foo = new Foo();
    public Ellipse ellipse;
}

public partial class MainWindow : Window
{
    List<Foo2> myList = new List<Foo2>();
    List<Tuple<int, int>> indicesList = new List<Tuple<int, int>>();

    public MainWindow()
    {
        InitializeComponent();

        for (int i = 0; i < 10; ++i)
            myList.Add(new Foo2 { ellipse = new Ellipse() });
        for (int i = 10; i < 20; ++i)
            myList.Add(new Foo2());
        indicesList.Add(new Tuple<int, int>(0, 9));
        indicesList.Add(new Tuple<int, int>(10, 19));
    }

    private void OnStart(object sender, RoutedEventArgs e)
    {
        foreach (var t in indicesList)
            ThreadPool.QueueUserWorkItem(new WaitCallback(Loop), t);
    }

    private void Loop(object o)
    {
        Tuple<int, int> indices = o as Tuple<int, int>;

        for(int i = indices.Item1; i <= indices.Item2; ++i)
        {
            myList[i].foo.DoCalc();
            if (myList[i].ellipse == null)
                continue;

            Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() => myList[i].ellipse.Fill = Brushes.Black));
        }
    }
}

The first half of items in myList has ellipse point to actual objects, while the second half points to null. Within Loop I check at every iteration if

Answers


John Peters February 2016

This could have something to do with closures...

Try this:

  var current = myList[i].foo.DoCalc();
            if (current.ellipse == null)
                continue;    
            Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, 
             new Action(() => current.ellipse.Fill = Brushes.Black));


Scott Chamberlain February 2016

You are running in to variable capture. The i that gets used in the invoke call will likely be indices.Item2 + 1 not the i value that it had at the time you did the BeginInvoke. You must copy i in to a local variable that is created new each loop itteration.

private void Loop(object o)
{
    Tuple<int, int> indices = o as Tuple<int, int>;

    for(int i = indices.Item1; i <= indices.Item2; ++i)
    {
        myList[i].foo.DoCalc();
        if (myList[i].ellipse == null)
            continue;

        int iLocal = i;
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() => myList[iLocal].ellipse.Fill = Brushes.Black));
    }
}

foreach prior to C# 5 had the same issue, see here for more info.

Post Status

Asked in February 2016
Viewed 3,654 times
Voted 6
Answered 2 times

Search




Leave an answer