如何正确等待调用Dispatcher.Invoke的多个线程在WPF应用程序中完成

时间:2010-09-29 19:06:05

标签: c# wpf multithreading

我有一个WPF应用程序启动3个线程,需要等待它们完成。我在这里阅读了许多处理此问题的帖子,但似乎没有解决线程代码调用Dispatcher.Invoke或Dispatcher.BeginInvoke的情况。如果我使用线程的Join()方法或ManualResetEvent,则线程会在Invoke调用上阻塞。这是一个丑陋的解决方案的简化代码片段似乎有效:

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();
...

非常感谢有关如何改进此代码的任何想法。看起来这应该是一个非常普遍的问题,但我似乎无法在任何地方找到它。

1 个答案:

答案 0 :(得分:8)

假设您可以使用.NET 4,我将以更清晰的方式向您展示如何避免跨线程共享可变状态(从而避免锁定)。

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;
    }
}

这门课的消费很容易:

// 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!");

该技术的说明

考虑不同的线程:而不是尝试将线程访问同步到共享数据(例如,您的点列表),而是在后台线程上做大量工作,但不要改变任何共享状态(例如,不要添加任何东西到积分榜)。对我们来说,这意味着循环遍历X和Y并找到Z,但不将它们添加到后台线程中的点列表中。一旦我们创建了数据,让UI线程知道我们已经完成了,并让他负责将这些点添加到列表中。

这种技术的优点是不共享任何可变状态 - 只有1个线程访问点集合。它还具有不需要任何锁定或显式同步的优点。

它有另一个重要特征:你的UI线程不会阻塞。这通常是一件好事,您不希望您的应用程序显示为冻结。如果要求阻止UI线程,我们必须稍微重做这个解决方案。