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

4

我有一个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();
...

如何改进这段代码,有任何想法都非常感谢。这似乎是一个非常普遍的问题,但我似乎无法在任何地方找到正确的解决方法。


你能澄清一下吗:你需要在UI线程上等待它们完成吗?(即阻塞UI线程?) - Judah Gabriel Himango
1个回答

9

假设你可以使用.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 线程我们已经完成了,并让他负责将点添加到列表中。

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

它还有另一个重要特点:您的 UI 线程不会被阻塞。这通常是一件好事,您不希望您的应用程序看起来像冻结了。如果阻止 UI 线程是必需的,则需要稍微重新设计此解决方案。


谢谢,Judah,非常好的解决方案。然而,关于UI阻塞问题,这是一款“亭式”应用程序,要求UI等待此操作完成(可能会显示进度条)。 - PIntag
你能否简单地显示一个占据整个屏幕的用户界面,以及一个进度条?阻塞UI线程会防止进度条渲染,因此你不希望实际上阻塞UI线程,你只是想在后台线程忙碌时防止用户输入,对吗? - Judah Gabriel Himango
是的,我认为这是一个不错的选择。非常感谢您的帮助 - 您的代码中有很多非常好的东西,我可以从中学习。 - PIntag
你的代码还有一个问题。"Task.Factory.ContinueWhenAll" 调用导致编译器抱怨 "OnAllTasksCompleted" 参数是无效的参数 "Argument 2: cannot convert from 'method group' to 'System.Action<System.Threading.Tasks.Task[]>'"。你知道这里出了什么问题导致了这个错误吗? - PIntag
是的。请确保您的任务调用AddPoint函数,其中AddPoint函数返回一个Point3D列表。 - Judah Gabriel Himango

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接