popov . dev

Главная

Библиотека

Статьи

Фоновые процессы...

Фоновые процессы в .NET на C#

Что такое фоновая обработка?

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

В данном материале, мы обсудим фоновые процессы .NET и то, как их использовать в приложении Windows Forms с использованием C#

Фоновые процессы (Background Workers)

В .NET класс BackgroundWorker предоставляет упрощенный способ выполнения фоновой обработки в приложениях Windows Forms. Его основная цель - упростить процесс выполнения трудоемких операций в отдельном потоке, сохраняя при этом адаптивность потока пользовательского интерфейса (UI).

Многопоточность необходима для создания адаптивных приложений, поскольку она позволяет выполнять задачи одновременно, предотвращает зависание пользовательского интерфейса во время длительных операций и максимизирует использование ресурсов. Однако многопоточность сопряжена с такими проблемами, как синхронизация, состояние гонки (конкуренция) и потокобезопасность, которыми необходимо тщательно управлять, чтобы избежать ошибок и неожиданного поведения. Такие инструменты, как класс BackgroundWorker в C#, предоставляют структурированный способ управления многопоточностью и создания мощных и удобных в использовании приложений.

Как BackgroundWorker обеспечивает фоновую обработку?

Он основан на событийной модели и предоставляет три основных события, которые вам необходимо обработать:

  • DoWork: здесь вы определяете код, который будет выполняться в фоновом режиме, и он будет выполняться в фоновом потоке.
  • RunWorkerCompleted: запускается по завершении фоновой работы. Он выполняется в потоке пользовательского интерфейса и позволяет обрабатывать результаты фоновой задачи и соответствующим образом обновлять пользовательский интерфейс.
  • ProgressChanged: позволяет вам обновлять элементы пользовательского интерфейса информацией о ходе выполнения во время выполнения фонового процесса.

Когда вы запускаете фоновую задачу с помощью BackgroundWorker, она автоматически создает новый поток для выполнения кода, определенного в событии DoWork. Этот отдельный поток гарантирует, что основной поток пользовательского интерфейса остается отзывчивым, даже если фоновая задача занимает значительное количество времени. Одной из ключевых особенностей BackgroundWorker является его способность взаимодействовать с потоком пользовательского интерфейса безопасным и контролируемым образом. Пока событие DoWork выполняется в фоновом потоке, вы можете использовать метод ReportProgress для отправки обновлений о ходе выполнения в поток пользовательского интерфейса. Затем в потоке пользовательского интерфейса запускается событие ProgressChanged, позволяющее обновлять элементы пользовательского интерфейса с информацией о ходе выполнения.

Работа с BackgroundWorker

Теперь давайте посмотрим на все это в действии на примере. Нам потребуется:

  • Visual Studio 2022
  • .NET 6
  • .NET Desktop Development (устанавливается инсталлятором Visual Studio)

Начнем с создания нового приложения Windows Forms.

Добавьте в конструктор следующие элементы управления, включая элемент управления BackgroundWorker:

Обратите внимание, что элемент управления BackgroundWorker не отображается в форме, потому что мы не взаимодействуем с ним через пользовательский интерфейс. Вот код конструктора:

namespace BackgroundWorkerExample {
    partial class Form1 {
        /// <summary>
        ///  Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        ///  Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing) {
            if (disposing && (components != null)) {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// <summary>
        ///  Required method for Designer support - do not modify
        ///  the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent() {
            processButton = new Button();
            cancelButton = new Button();
            progressBar = new ProgressBar();
            progressLabel = new Label();
            backgroundWorker = new System.ComponentModel.BackgroundWorker();
            randomNumberButton = new Button();
            randomNumberLabel = new Label();
            SuspendLayout();
            // 
            // processButton
            // 
            processButton.Location = new Point(12, 12);
            processButton.Name = "processButton";
            processButton.Size = new Size(138, 29);
            processButton.TabIndex = 0;
            processButton.Text = "Process";
            processButton.UseVisualStyleBackColor = true;
            processButton.Click += processButton_Click;
            // 
            // cancelButton
            // 
            cancelButton.Location = new Point(174, 12);
            cancelButton.Name = "cancelButton";
            cancelButton.Size = new Size(139, 29);
            cancelButton.TabIndex = 1;
            cancelButton.Text = "Cancel";
            cancelButton.UseVisualStyleBackColor = true;
            cancelButton.Click += cancelButton_Click;
            // 
            // progressBar
            // 
            progressBar.Location = new Point(12, 53);
            progressBar.Name = "progressBar";
            progressBar.Size = new Size(301, 29);
            progressBar.TabIndex = 2;
            // 
            // progressLabel
            // 
            progressLabel.AutoSize = true;
            progressLabel.Location = new Point(12, 96);
            progressLabel.Name = "progressLabel";
            progressLabel.Size = new Size(29, 20);
            progressLabel.TabIndex = 3;
            progressLabel.Text = "0%";
            // 
            // backgroundWorker
            // 
            backgroundWorker.WorkerReportsProgress = true;
            backgroundWorker.WorkerSupportsCancellation = true;
            backgroundWorker.DoWork += backgroundWorker_DoWork;
            backgroundWorker.ProgressChanged += backgroundWorker_ProgressChanged;
            backgroundWorker.RunWorkerCompleted += backgroundWorker_RunWorkerCompleted;
            // 
            // randomNumberButton
            // 
            randomNumberButton.Location = new Point(12, 138);
            randomNumberButton.Name = "randomNumberButton";
            randomNumberButton.Size = new Size(138, 29);
            randomNumberButton.TabIndex = 4;
            randomNumberButton.Text = "Random Number";
            randomNumberButton.UseVisualStyleBackColor = true;
            randomNumberButton.Click += button1_Click;
            // 
            // randomNumberLabel
            // 
            randomNumberLabel.AutoSize = true;
            randomNumberLabel.Location = new Point(203, 142);
            randomNumberLabel.Name = "randomNumberLabel";
            randomNumberLabel.Size = new Size(17, 20);
            randomNumberLabel.TabIndex = 5;
            randomNumberLabel.Text = "0";
            // 
            // Form1
            // 
            AutoScaleDimensions = new SizeF(8F, 20F);
            AutoScaleMode = AutoScaleMode.Font;
            ClientSize = new Size(330, 187);
            Controls.Add(randomNumberLabel);
            Controls.Add(randomNumberButton);
            Controls.Add(progressLabel);
            Controls.Add(progressBar);
            Controls.Add(cancelButton);
            Controls.Add(processButton);
            Name = "Form1";
            Text = "Form1";
            ResumeLayout(false);
            PerformLayout();
        }

        #endregion

        private Button processButton;
        private Button cancelButton;
        private ProgressBar progressBar;
        private Label progressLabel;
        private System.ComponentModel.BackgroundWorker backgroundWorker;
        private Button randomNumberButton;
        private Label randomNumberLabel;
    }
}

Теперь обновите свойства WorkerReportProgress и WorkerSupportsCancellation элемента управления BackgroundWorker на значение true. Они нужны нам для предоставления пользовательскому интерфейсу данных о ходе выполнения, а также для отмены фонового процесса при необходимости.

Перейдите на вкладку события в свойствах BackgroundWorker и щелкните по каждому из 3 событий, чтобы сгенерировать обработчики событий:

Это приведет к созданию пустых обработчиков событий, для которых мы предоставим реализацию.

Чтобы использовать BackgroundWorker, вы просто указываете ему, какой нагруженный метод воркера следует выполнить в фоновом режиме в обработчике событий DoWork.

private void backgroundWorker_DoWork(object sender,
    System.ComponentModel.DoWorkEventArgs e) {
    for (int i = 0; i <= 100; i++) {
        Thread.Sleep(100);
        backgroundWorker.ReportProgress(i);

        if (backgroundWorker.CancellationPending) {
            e.Cancel = true;
            backgroundWorker.ReportProgress(0);
            return;
        }
    }

    e.Result = "Completed";
}

Событие запускает цикл for и задерживается на 100 миллисекунд на каждой итерации. Это имитирует трудоемкую операцию, такую как HTTP-запрос на загрузку огромного файла. Он также информирует основной поток о ходе выполнения операции с помощью метода ReportProgress. Если была инициирована отмена, будет установлено CancellationProperty в BackgroundWorker, которое может быть обработано соответствующим образом. Если цикл for завершается без какой-либо отмены, Result в DoWorkEventArgs обновляется значением. Это событие будет передано в аргументы обработчика событий RunWorkerCompleted как RunWorkerCompletedEventArgs.

Теперь мы можем запустить событие DoWork, вызвав метод RunWorkerAsync внутри обработчика событий processButton_Click.

private void processButton_Click(object sender, EventArgs e) {
    if (!backgroundWorker.IsBusy) {
        backgroundWorker.RunWorkerAsync();
    }
}

Пока рабочий метод выполняется в фоновом режиме, основной поток может безопасно обновляться в соответствии с ходом выполнения с помощью обработчика событий ProgressChanged.

private void backgroundWorker_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e) {
    progressBar.Value = e.ProgressPercentage;
    progressLabel.Text = e.ProgressPercentage.ToString() + "%";
}

Вызывающий поток продолжает работать в обычном режиме, в то время как рабочий метод выполняется асинхронно. Когда метод завершается, BackgroundWorker предупреждает вызывающий поток, запуская событие RunWorkerCompleted, которое необязательно содержит результаты операции.

private void backgroundWorker_RunWorkerCompleted(object sender,
    System.ComponentModel.RunWorkerCompletedEventArgs e) {
    if (e.Cancelled) {
        progressLabel.Text = "Processing cancelled";
    } else if (e.Error is not null) {
        progressLabel.Text = e.Error.Message;
    } else {
        progressLabel.Text = e.Result?.ToString();
    }
}

Чтобы отменить фоновую обработку, пока она еще выполняется, вызовите метод CancelAsync.

private void cancelButton_Click(object sender, EventArgs e) {
    if (backgroundWorker.IsBusy) {
        backgroundWorker.CancelAsync();
    }
}

Чтобы продемонстрировать, насколько мощным является BackgroundWorker, давайте добавим некоторый код для генерации случайного числа, которое будет сгенерировано в основном потоке во время выполнения фоновой обработки.

private void randomNumberButton_Click(object sender, EventArgs e) {
    randomNumberLabel.Text = new Random().Next(0, 1000).ToString();
}

Обработка исключений

Событие DoWork может вызывать исключения, которые автоматически перехватываются и помещаются в свойство Error события RunWorkerCompletedEventArgs, которое передается в аргументы обработчика событий RunWorkerCompleted. Вы можете использовать блок try-catch для обработки исключения, но имейте в виду, что если исключение не генерируется в блоке catch, RunWorkerCompletedEventArgs.Error будет равен null.

Обновите обработчик событий DoWork следующим образом:

private void backgroundWorker_DoWork(object sender,
    System.ComponentModel.DoWorkEventArgs e) {
    try {
        for (int i = 0; i <= 100; i++) {
            Thread.Sleep(100);
            backgroundWorker.ReportProgress(i);

            if (backgroundWorker.CancellationPending) {
                e.Cancel = true;
                backgroundWorker.ReportProgress(0);
                return;
            }
                    
            throw new InvalidOperationException("Random exception.");
        }
    } catch {
        throw;
    }

    e.Result = "Completed";
}

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

Заключение

Мы можем использовать компонент BackgroundWorker для создания адаптивных приложений Windows Forms, выделяя трудоемкие задачи в отдельный поток, делая пользовательский интерфейс более удобным для пользователя и избегая зависания или отсутствия ответа. BackgroundWorker обеспечивает абстрагирование от многопоточности в .NET и упрощает создание адаптивных приложений, не беспокоясь об управлении потоками.

Хотя BackgroundWorker является ценным инструментом, в современных версиях C# и .NET доступны более новые опции, такие как библиотека параллельных задач (TPL) и шаблоны async/await. Эти альтернативы предоставляют более продвинутые и гибкие подходы к управлению асинхронными задачами.

Комментарии

Для того чтобы оставить свое мнение, необходимо зарегистрироваться на сайте