views:

76

answers:

1

I have a WPF application that kicks off 3 threads and needs to wait for them to finish. I have read many posts here that deal with this but none seem to address the situation where the thread code calls Dispatcher.Invoke or Dispatcher.BeginInvoke. If I use the thread's Join() method or a ManualResetEvent, the thread blocks on the Invoke call. Here's a simplified code snippet of an ugly solution that seems to work:

class PointCloud
{
    private Point3DCollection points = new Point3DCollection(1000);
    private volatile bool[] tDone = { false, false, false };
    private static readonly object _locker = new object();

    public ModelVisual3D BuildPointCloud()
    {
        ...
        Thread t1 = new Thread(() => AddPoints(0, 0, 192));
        Thread t2 = new Thread(() => AddPoints(1, 193, 384));
        Thread t3 = new Thread(() => AddPoints(2, 385, 576));
        t1.Start();
        t2.Start();
        t3.Start();

        while (!tDone[0] || !tDone[1] || !tDone[2]) 
        {
            Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background, new ThreadStart(delegate { }));
            Thread.Sleep(1);
        }

        ...
    }

    private void AddPoints(int scanNum, int x, int y)
    {
        for (int i = 0; i < x; i++)
        {
            for (int j = 0; j < y; j++)
            {
                z = FindZ(x, y);

                if (z == GOOD_VALUE)
                {
                    Application.Current.Dispatcher.Invoke(DispatcherPriority.Normal,
                      (ThreadStart)delegate()
                      {
                          Point3D newPoint = new Point3D(x, y, z);
                          lock (_locker)
                          {
                              points.Add(newPoint);
                          }
                      }
                  );
                } 
            }
        }
        tDone[scanNum] = true;
    }
}

from the main WPF thread...
PointCloud pc = new PointCloud();
ModelVisual3D = pc.BuildPointCloud();
...

Any ideas about how to improve this code would be much appreciated. It seems like this should be a very common problem, but I can't seem to find it properly addressed anywhere.

+3  A: 

Assuming you can use .NET 4, I'm going to show you how to do this in a much cleaner way that avoids sharing mutable state across threads (and thus, avoids locking).

class PointCloud
{
    public Point3DCollection Points { get; private set; }

    public event EventHandler AllThreadsCompleted;

    public PointCloud()
    {
        this.Points = new Point3DCollection(1000);

        var task1 = Task.Factory.StartNew(() => AddPoints(0, 0, 192));
        var task2 = Task.Factory.StartNew(() => AddPoints(1, 193, 384));
        var task3 = Task.Factory.StartNew(() => AddPoints(2, 385, 576));
        Task.Factory.ContinueWhenAll(
            new[] { task1, task2, task3 }, 
            OnAllTasksCompleted, // Call this method when all tasks finish.
            CancellationToken.None, 
            TaskContinuationOptions.None,
            TaskScheduler.FromCurrentSynchronizationContext()); // Finish on UI thread.
    }

    private void OnAllTasksCompleted(Task<List<Point3D>>[] completedTasks)
    {
        // Now that we've got our points, add them to our collection.
        foreach (var task in completedTasks)
        {
            task.Result.ForEach(point => this.points.Add(point));
        }

        // Raise the AllThreadsCompleted event.
        if (AllThreadsCompleted != null)
        {
            AllThreadsCompleted(this, EventArgs.Empty);
        }
    }

    private List<Point3D> AddPoints(int scanNum, int x, int y)
    {
       const int goodValue = 42;
       var result = new List<Point3D>(500);
       var points = from pointX in Enumerable.Range(0, x)
                    from pointY in Enumerable.Range(0, y)
                    let pointZ = FindZ(pointX, pointY)
                    where pointZ == goodValue
                    select new Point3D(pointX, pointX, pointZ);
       result.AddRange(points);
       return result;
    }
}

Consumption of this class is easy:

// On main WPF UI thread:
var cloud = new PointCloud();
cloud.AllThreadsCompleted += (sender, e) => MessageBox.Show("all threads done! There are " + cloud.Points.Count.ToString() + " points!");

Explanation of this technique

Think about threading differently: instead of trying to synchronize thread access to shared data (e.g. your point list), instead do heavy lifting on the background thread but don't mutate any shared state (e.g. don't add anything to the points list). For us, this means looping over X and Y and finding Z, but not adding them to the points list in the background thread. Once we've created the data, let the UI thread know we're done and let him take care of adding the points to the list.

This technique has the advantage of not sharing any mutable state -- only 1 thread accesses the points collection. It also has the advantage of not requiring any locks or explicit synchronization.

It has another important characteristic: your UI thread won't block. This is generally a good thing, you don't want your app to appear frozen. If blocking the UI thread is a requirement, we'd have to rework this solution a bit.

Judah Himango
Thanks, Judah, excellent solution. However, regarding the UI blocking issue, this is a "kiosk-style" application and it is a requirement that the UI waits for this operation to be performed (perhaps with a progress bar displayed).
PIntag
Could you simply display some UI that takes up the whole screen, along with a progress bar? Blocking the UI thread will prevent a progress bar from rendering, so you don't want that to actually block the UI thread, you just want to prevent user input while the background threads are busy, right?
Judah Himango
Yes, I think that is a good route to go. Thanks so much for your help - there's lots of really good stuff in your code that I can learn from.
PIntag
Glad to help. Good luck.
Judah Himango
One more question on your code. The "Task.Factory.ContinueWhenAll" call is causing the compiler to complain that the "OnAllTasksCompleted" parameter is an invalid argument "Argument 2: cannot convert from 'method group' to 'System.Action<System.Threading.Tasks.Task[]>'. Any idea what is going on here to cause that error?
PIntag
Yes. Make sure your tasks are calling AddPoint, where AddPoint returns a list of Point3D.
Judah Himango
Oh, okay - I didn't have that coded yet. Thanks.
PIntag