Как правильно дождаться завершения нескольких потоков, вызывающих Dispatcher.Invoke, в приложении WPF - PullRequest
4 голосов
/ 29 сентября 2010

У меня есть приложение 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 Ответ

8 голосов
/ 30 сентября 2010

Предполагая, что вы можете использовать .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, но не добавление их в список точек в фоновом потоке. После того, как мы создали данные, сообщите потоку пользовательского интерфейса, что мы закончили, и пусть он позаботится о добавлении точек в список.

Преимущество этого метода состоит в том, что он не разделяет никакое изменяемое состояние - только 1 поток получает доступ к коллекции точек. Он также имеет то преимущество, что не требует никаких блокировок или явной синхронизации.

У него есть еще одна важная характеристика: ваш поток пользовательского интерфейса не будет блокироваться. Как правило, это хорошо, вы не хотите, чтобы ваше приложение выглядело замороженным. Если блокирование потока пользовательского интерфейса является требованием, нам придется немного переработать это решение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...