Использование BackgroundWorker для выполнения двух методов один за другим WPF/C#

В моей программе у меня есть два метода, выполнение которых занимает некоторое время, около нескольких минут каждый. Пока эти методы выполняются, я отображаю индикатор выполнения в отдельном окне, который показывает ход выполнения каждого метода. Мои два метода находятся в статическом классе Utility. Они выглядят следующим образом:

public static class Utility
{
    public static bool TimeConsumingMethodOne(object sender)
    {
        for (int i = 1; i <= 100; i++)
        {
            Thread.Sleep(100);
            (sender as BackgroundWorker).ReportProgress(i);
        }
        return true;
    }

    public static bool TimeConsumingMethodTwo(object sender)
    {
        for (int i = 1; i <= 100; i++)
        {
            Thread.Sleep(50);
            (sender as BackgroundWorker).ReportProgress(i);
        }
        return true;
    }
}

Прочитав похожие вопросы в SO, я узнал, что должен использовать BackgroundWorker и использовать RunWorkerCompleted(), чтобы увидеть, когда рабочий завершит свою работу. Итак, в моем Main() я использовал BackgroundWorer() и подписался на метод RunWorkerCompleted(). Моя цель здесь - сначала запустить TimeConsumingMethodOne() (и отобразить прогресс во время работы), затем после завершения запустить TimeConsumingMethodTwo() и снова показать прогресс, а когда это будет завершено, вывести окно сообщения (которое имитирует некоторые другие работы в моей программе) . Мой Main() выглядит следующим образом:

public partial class MainWindow : Window
{
    public enum MethodType
    {
        One,
        Two
    }

    private BackgroundWorker worker = null;
    private AutoResetEvent _resetEventOne = new AutoResetEvent(false);
    private AutoResetEvent _resetEventTwo = new AutoResetEvent(false);

    private ProgressBarWindow pbWindowOne = null;
    private ProgressBarWindow pbWindowTwo = null;

    public MainWindow()
    {
        InitializeComponent();
    }

    private void btnRun_Click(object sender, RoutedEventArgs e)
    {
        RunMethodCallers(sender, MethodType.One);
        _resetEventOne.WaitOne();
        RunMethodCallers(sender, MethodType.Two);
        _resetEventTwo.WaitOne();
        MessageBox.Show("COMPLETED!");
    }

    private void RunMethodCallers(object sender, MethodType type)
    {
        worker = new BackgroundWorker();
        worker.WorkerReportsProgress = true;
        switch (type)
        {
            case MethodType.One:
                worker.DoWork += MethodOneCaller;
                worker.ProgressChanged += worker_ProgressChangedOne;
                worker.RunWorkerCompleted += worker_RunWorkerCompletedOne;
                break;
            case MethodType.Two:
                worker.DoWork += MethodTwoCaller;
                worker.ProgressChanged += worker_ProgressChangedTwo;
                worker.RunWorkerCompleted += worker_RunWorkerCompletedTwo;
                break;
        }
        worker.RunWorkerAsync();
    }


    private void MethodOneCaller(object sender, DoWorkEventArgs e)
    {
        Dispatcher.Invoke(() =>
        {
            pbWindowOne = new ProgressBarWindow("Running Method One");
            pbWindowOne.Owner = this;
            pbWindowOne.Show();
        });

        Utility.TimeConsumingMethodOne(sender);
    }

    private void MethodTwoCaller(object sender, DoWorkEventArgs e)
    {
        Dispatcher.Invoke(() =>
        {
            pbWindowTwo = new ProgressBarWindow("Running Method Two");
            pbWindowTwo.Owner = this;
            pbWindowTwo.Show();
        });

        Utility.TimeConsumingMethodTwo(sender);
    }

    private void worker_RunWorkerCompletedOne(object sender, RunWorkerCompletedEventArgs e)
    {
        _resetEventOne.Set();
    }

    private void worker_RunWorkerCompletedTwo(object sender, RunWorkerCompletedEventArgs e)
    {
        _resetEventTwo.Set();
    }

    private void worker_ProgressChangedOne(object sender, ProgressChangedEventArgs e)
    {
        pbWindowOne.SetProgressUpdate(e.ProgressPercentage);
    }

    private void worker_ProgressChangedTwo(object sender, ProgressChangedEventArgs e)
    {
        pbWindowTwo.SetProgressUpdate(e.ProgressPercentage);
    }
}

Теперь у меня проблема: когда я использую _resetEventOne.WaitOne(); пользовательский интерфейс зависает. Если я уберу эти два ожидания, оба метода будут работать асинхронно, а выполнение продолжится и выведет MessageBox еще до завершения этих двух методов.

Что я делаю неправильно? Как мне заставить программу завершить мой первый BackgroundWorker, а затем перейти к следующему, а затем, когда это будет сделано, вывести MessageBox?


person Sach    schedule 21.07.2017    source источник


Ответы (2)


Теперь у меня проблема: когда я использую _resetEventOne.WaitOne(); пользовательский интерфейс зависает. Если я уберу эти два ожидания, оба метода будут работать асинхронно, а выполнение продолжится и выведет MessageBox еще до завершения этих двух методов.

Что я делаю неправильно?

Когда вы вызываете WaitOne(), вы блокируете поток пользовательского интерфейса, вызывая зависание пользовательского интерфейса. Если вы уберете этот вызов, то, конечно, вы запустите сразу обоих воркеров.

Есть несколько разных подходов к вашему вопросу. Один из них — придерживаться вашей текущей реализации и просто исправить самый минимум, чтобы заставить ее работать. При этом вам нужно будет выполнить фактический оператор next в обработчике RunWorkerCompleted вместо использования события для ожидания выполнения обработчика.

Это выглядит так:

public partial class MainWindow : Window
{
    public enum MethodType
    {
        One,
        Two
    }

    private BackgroundWorker worker = null;

    private ProgressBarWindow pbWindowOne = null;
    private ProgressBarWindow pbWindowTwo = null;

    public MainWindow()
    {
        InitializeComponent();
    }

    private void btnRun_Click(object sender, RoutedEventArgs e)
    {
        RunMethodCallers(sender, MethodType.One);
    }

    private void RunMethodCallers(object sender, MethodType type)
    {
        worker = new BackgroundWorker();
        worker.WorkerReportsProgress = true;
        switch (type)
        {
            case MethodType.One:
                worker.DoWork += MethodOneCaller;
                worker.ProgressChanged += worker_ProgressChangedOne;
                worker.RunWorkerCompleted += worker_RunWorkerCompletedOne;
                break;
            case MethodType.Two:
                worker.DoWork += MethodTwoCaller;
                worker.ProgressChanged += worker_ProgressChangedTwo;
                worker.RunWorkerCompleted += worker_RunWorkerCompletedTwo;
                break;
        }
        worker.RunWorkerAsync();
    }

    private void MethodOneCaller(object sender, DoWorkEventArgs e)
    {
        Dispatcher.Invoke(() =>
        {
            pbWindowOne = new ProgressBarWindow("Running Method One");
            pbWindowOne.Owner = this;
            pbWindowOne.Show();
        });

        Utility.TimeConsumingMethodOne(sender);
    }

    private void MethodTwoCaller(object sender, DoWorkEventArgs e)
    {
        Dispatcher.Invoke(() =>
        {
            pbWindowTwo = new ProgressBarWindow("Running Method Two");
            pbWindowTwo.Owner = this;
            pbWindowTwo.Show();
        });

        Utility.TimeConsumingMethodTwo(sender);
    }

    private void worker_RunWorkerCompletedOne(object sender, RunWorkerCompletedEventArgs e)
    {
        RunMethodCallers(sender, MethodType.Two);
    }

    private void worker_RunWorkerCompletedTwo(object sender, RunWorkerCompletedEventArgs e)
    {
        MessageBox.Show("COMPLETED!");
    }

    private void worker_ProgressChangedOne(object sender, ProgressChangedEventArgs e)
    {
        pbWindowOne.SetProgressUpdate(e.ProgressPercentage);
    }

    private void worker_ProgressChangedTwo(object sender, ProgressChangedEventArgs e)
    {
        pbWindowTwo.SetProgressUpdate(e.ProgressPercentage);
    }
}

Тем не менее, BackgroundWorker устарел благодаря более новому API, основанному на задачах, с async и await. С небольшими изменениями в вашем коде его можно адаптировать для использования этой новой идиомы:

public partial class MainWindow : Window
{
    public enum MethodType
    {
        One,
        Two
    }

    private ProgressBarWindow pbWindowOne = null;
    private ProgressBarWindow pbWindowTwo = null;

    public MainWindow()
    {
        InitializeComponent();
    }

    private async void btnRun_Click(object sender, RoutedEventArgs e)
    {
        await RunMethodCallers(sender, MethodType.One);
        await RunMethodCallers(sender, MethodType.Two);
        MessageBox.Show("COMPLETED!");
    }

    private async Task RunMethodCallers(object sender, MethodType type)
    {
        IProgress<int> progress;

        switch (type)
        {
            case MethodType.One:
                progress = new Progress<int>(i => pbWindowOne.SetProgressUpdate(i));
                await Task.Run(() => MethodOneCaller(progress));
                break;
            case MethodType.Two:
                progress = new Progress<int>(i => pbWindowTwo.SetProgressUpdate(i));
                await Task.Run(() => MethodTwoCaller(progress));
                break;
        }
    }

    private void MethodOneCaller(IProgress<int> progress)
    {
        Dispatcher.Invoke(() =>
        {
            pbWindowOne = new ProgressBarWindow("Running Method One");
            pbWindowOne.Owner = this;
            pbWindowOne.Show();
        });

        Utility.TimeConsumingMethodOne(progress);
    }

    private void MethodTwoCaller(IProgress<int> progress)
    {
        Dispatcher.Invoke(() =>
        {
            pbWindowTwo = new ProgressBarWindow("Running Method Two");
            pbWindowTwo.Owner = this;
            pbWindowTwo.Show();
        });

        Utility.TimeConsumingMethodTwo(progress);
    }
}

Чтобы сделать это, также требуется небольшая корректировка класса Utility:

static class Utility
{
    public static bool TimeConsumingMethodOne(IProgress<int> progress)
    {
        for (int i = 1; i <= 100; i++)
        {
            Thread.Sleep(100);
            progress.Report(i);
        }
        return true;
    }

    public static bool TimeConsumingMethodTwo(IProgress<int> progress)
    {
        for (int i = 1; i <= 100; i++)
        {
            Thread.Sleep(50);
            progress.Report(i);
        }
        return true;
    }
}

То есть класс Progress<T> заменяет событие BackgroundWorker.ProgressChanged и метод ReportProgress().

Обратите внимание, что благодаря приведенному выше код стал значительно короче, проще и написан более прямым образом (т. е. связанные операторы теперь находятся друг с другом в одном методе).

Приведенный вами пример обязательно упрощен. Это прекрасно, но это означает, что здесь неизвестно, что представляет собой метод Thread.Sleep(). На самом деле, во многих случаях такого рода вещи можно подвергнуть дальнейшему рефакторингу, чтобы асинхронно выполнялась только длительная работа. Иногда это может еще больше упростить отчет о ходе выполнения, потому что это можно сделать после await-записи каждого отдельного асинхронно выполняемого рабочего компонента.

Например, предположим, что работа в цикле либо изначально асинхронна, либо достаточно затратна, поэтому разумно использовать Task.Run() для выполнения каждой итерации цикла. Для того же, что можно представить с помощью Task.Delay():

static class Utility
{
    public static async Task<bool> TimeConsumingMethodOne(Action<int> progress)
    {
        for (int i = 1; i <= 100; i++)
        {
            await Task.Delay(100);
            progress(i);
        }
        return true;
    }

    public static async Task<bool> TimeConsumingMethodTwo(Action<int> progress)
    {
        for (int i = 1; i <= 100; i++)
        {
            await Task.Delay(50);
            progress(i);
        }
        return true;
    }
}

В приведенном выше я также не использую Progress<T>. Просто простой делегат Action<int>, который вызывающая сторона может использовать по своему усмотрению.

И с этим изменением ваш код окна становится еще проще:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private async void btnRun_Click(object sender, RoutedEventArgs e)
    {
        await MethodOneCaller();
        await MethodTwoCaller();
        MessageBox.Show("COMPLETED!");
    }

    private async Task MethodOneCaller()
    {
        ProgressBarWindow pbWindowOne =
            new ProgressBarWindow("Running Method One") { Owner = this };
        pbWindowOne.Show();

        await Utility.TimeConsumingMethodOne(i => pbWindowOne.SetProgressUpdate(i));
    }

    private async Task MethodTwoCaller()
    {
        ProgressBarWindow pbWindowTwo =
            new ProgressBarWindow("Running Method Two") { Owner = this };

        pbWindowTwo.Show();

        await Utility.TimeConsumingMethodTwo(i => pbWindowTwo.SetProgressUpdate(i));
    }
}

Конечно, я воспользовался возможностью удалить перечисление MethodType и просто вызывать методы напрямую, что еще больше сократило код. Но даже если все, что вы сделали, это избегали использования Dispatcher.Invoke(), это все равно сильно упрощает код.

В дополнение ко всему этому, если бы вы использовали привязку данных для представления состояния выполнения вместо того, чтобы устанавливать значение напрямую, WPF неявно обрабатывал бы вызов между потоками для вас, так что класс Progress<T> даже не требуется, даже если вы можете не рефакторить код класса Utility, чтобы он сам стал async.

Но это незначительные улучшения по сравнению с отходом от BackgroundWorker. Я рекомендую это сделать, но менее важно, тратите ли вы время на эти дальнейшие уточнения.

person Peter Duniho    schedule 21.07.2017
comment
Это отличный ответ, спасибо! Да, первый упомянутый вами метод достигает цели, но беспорядочно и ограниченно; Я использовал только два метода, чтобы опубликовать этот вопрос, но моя программа на самом деле имеет более двух методов, и вызов каждого из них в событии завершения предыдущего метода не идеален. Мне гораздо больше нравится второй подход; это аккуратно, интуитивно понятно и расширяемо. Я подумаю об использовании последнего варианта с Action‹›, но пока думаю использовать второй вариант. Спасибо еще раз! - person Sach; 22.07.2017

Вариант, который я предпочитаю, - иметь эти 2 метода в другом потоке и использовать цикл while, чтобы проверить, работает ли поток, и если он использует Task.Delay() EG.

private async void BlahBahBlahAsync()
    {
        Thread testThread = new Thread(delegate () { });
        newThread = new Thread(delegate ()
        {
            Timeconsuming();
        });
        newThread.Start();
        while (testThread.IsAlive)
        {
            await Task.Delay(50);
        }
    }

    private void Timeconsuming()
    {
        // stuff that takes a while
    }
person Ricky Divjakovski    schedule 21.07.2017
comment
Забыл упомянуть, если вам нужен вывод из него, вы должны добавить testThread.Join() после цикла while - person Ricky Divjakovski; 22.07.2017