Нормально ли, что параллельная обработка приводит к заиканию пользовательского интерфейса?

У меня есть приложение Windows Forms, которое копирует и изменяет размеры ссылок на изображения в генеалогическом файле GEDCOM. Пользователь выбирает файл и выходной каталог, а также параметры изменения размера из основной формы, которая затем открывает другую форму в виде диалогового окна, содержащего метку, индикатор выполнения и кнопку. Я обновляю приложение, чтобы использовать новые асинхронные функции в .NET 4.5, а также модифицирую его, чтобы использовать параллельную обработку. Все работает нормально, за исключением того, что я заметил, что отзывчивость пользовательского интерфейса немного прерывистая (заикается); Если я не обновляю метку сообщения с процентами, то это намного плавнее. Кроме того, когда я отменяю задачу, пользовательский интерфейс зависает от 1 до 15 секунд. Приложение предназначено только для моего личного использования, так что это не так уж важно, но мне любопытно, что может быть причиной проблемы и каков рекомендуемый способ ее решения. Параллельная обработка просто перегружает ЦП из-за слишком большого количества потоков для обработки? Я попытался добавить Thread.Sleep(100) к каждой итерации цикла, и, похоже, это немного помогло.

Вот минимальная версия приложения, которая все еще вызывает проблемы. Чтобы воспроизвести:

  1. Создайте новое приложение Windows Forms с формой ниже.
  2. Создайте каталог с кучей изображений в формате jpeg (более 50 изображений).
  3. Замените переменные _SourceDirectoryPath и _DestinationDirectoryPath вашими каталогами.
  4. Запустить приложение

Designer.cs:

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()
    {
        this.lblMessage = new System.Windows.Forms.Label();
        this.pgProgressBar = new System.Windows.Forms.ProgressBar();
        this.btnStart = new System.Windows.Forms.Button();
        this.btnCancel = new System.Windows.Forms.Button();
        this.SuspendLayout();
        // 
        // lblMessage
        // 
        this.lblMessage.AutoSize = true;
        this.lblMessage.Location = new System.Drawing.Point(32, 25);
        this.lblMessage.Name = "lblMessage";
        this.lblMessage.Size = new System.Drawing.Size(0, 13);
        this.lblMessage.TabIndex = 0;
        // 
        // pgProgressBar
        // 
        this.pgProgressBar.Location = new System.Drawing.Point(35, 51);
        this.pgProgressBar.Name = "pgProgressBar";
        this.pgProgressBar.Size = new System.Drawing.Size(253, 23);
        this.pgProgressBar.TabIndex = 1;
        // 
        // btnStart
        // 
        this.btnStart.Location = new System.Drawing.Point(132, 97);
        this.btnStart.Name = "btnStart";
        this.btnStart.Size = new System.Drawing.Size(75, 23);
        this.btnStart.TabIndex = 2;
        this.btnStart.Text = "Start";
        this.btnStart.UseVisualStyleBackColor = true;
        this.btnStart.Click += new System.EventHandler(this.btnStart_Click);
        // 
        // btnCancel
        // 
        this.btnCancel.Location = new System.Drawing.Point(213, 97);
        this.btnCancel.Name = "btnCancel";
        this.btnCancel.Size = new System.Drawing.Size(75, 23);
        this.btnCancel.TabIndex = 3;
        this.btnCancel.Text = "Cancel";
        this.btnCancel.UseVisualStyleBackColor = true;
        this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
        // 
        // Form1
        // 
        this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        this.ClientSize = new System.Drawing.Size(315, 149);
        this.Controls.Add(this.btnCancel);
        this.Controls.Add(this.btnStart);
        this.Controls.Add(this.pgProgressBar);
        this.Controls.Add(this.lblMessage);
        this.Name = "Form1";
        this.Text = "Form1";
        this.ResumeLayout(false);
        this.PerformLayout();

    }

    #endregion

    private System.Windows.Forms.Label lblMessage;
    private System.Windows.Forms.ProgressBar pgProgressBar;
    private System.Windows.Forms.Button btnStart;
    private System.Windows.Forms.Button btnCancel;
}

Код:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

public partial class Form1 : Form
{
    private CancellationTokenSource _CancelSource;
    private string _SourceDirectoryPath = @"Your\Source\Directory";
    private string _DestinationDirectoryPath = @"Your\Destination\Directory";

    public Form1()
    {
        InitializeComponent();
        lblMessage.Text = "Click Start to begin extracting images";
        btnCancel.Enabled = false;
        _CancelSource = new CancellationTokenSource();
    }

    private async void btnStart_Click(object sender, EventArgs e)
    {
        btnStart.Enabled = false;
        btnCancel.Enabled = true;

        List<string> files = await Task.Run(() => Directory.GetFiles(_SourceDirectoryPath, "*.jpg").ToList());

        // scan/extract files
        Progress<int> progress = new Progress<int>(UpdateProgress);
        int result = await Task.Run(() => ExtractFiles(files, progress, _CancelSource.Token));

        if (_CancelSource.IsCancellationRequested)
        {
            lblMessage.Text = "Extraction cancelled by user.";
        }
        else
        {
            lblMessage.Text = string.Format("Extraction Complete: {0} files extracted.", result);
        }
        btnStart.Enabled = true;
        btnCancel.Enabled = false;
    }

    private void btnCancel_Click(object sender, EventArgs e)
    {
        lblMessage.Text = "Cancelling...";
        btnCancel.Enabled = false;
        _CancelSource.Cancel();
    }

    private void UpdateProgress(int value)
    {
        lblMessage.Text = string.Format("Extracting files: {0}%", value);
        pgProgressBar.Value = value;
    }

    public int ExtractFiles(List<string> fileReferences, IProgress<int> progress, CancellationToken cancelToken)
    {
        double totalFiles = fileReferences.Count;
        int processedCount = 0;
        int extractedCount = 0;
        int previousPercent = 0;
        Directory.CreateDirectory(_DestinationDirectoryPath);

        Parallel.ForEach(fileReferences, (reference, state) =>
        {
            if (cancelToken.IsCancellationRequested)
            {
                state.Break();
            }

            string fileName = Path.GetFileName(reference);
            string filePath = Path.Combine(_DestinationDirectoryPath, fileName);

            using (Image image = Image.FromFile(reference))
            {
                using (Image newImage = ResizeImage(image, 1000, 1000))
                {
                    newImage.Save(filePath);
                    Interlocked.Increment(ref extractedCount);
                }
            }

            Interlocked.Increment(ref processedCount);
            int percent = (int)(processedCount / totalFiles * 100);
            if (percent > previousPercent)
            {
                progress.Report(percent);
                Interlocked.Exchange(ref previousPercent, percent);
            }
        });

        return extractedCount;
    }

    public Image ResizeImage(Image image, int maxWidth, int maxHeight)
    {
        Image newImage = null;

        if (image.Width > maxWidth || image.Height > maxHeight)
        {
            double widthRatio = (double)maxWidth / (double)image.Width;
            double heightRatio = (double)maxHeight / (double)image.Height;
            double ratio = Math.Min(widthRatio, heightRatio);
            int newWidth = (int)(image.Width * ratio);
            int newHeight = (int)(image.Height * ratio);

            newImage = new Bitmap(newWidth, newHeight);
            using (Graphics graphic = Graphics.FromImage(newImage))
            {
                graphic.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
                graphic.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
                graphic.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
                graphic.DrawImage(image, 0, 0, newWidth, newHeight);
            }
        }

        return newImage;
    }
}

person Matt P.    schedule 23.11.2015    source источник
comment
Боковое примечание: не бросайте внутрь параллельный foreach, чтобы сломаться. Для этого уже есть опция перерыва. Поместите оператор if CancelationTokenRequested, а затем state.Break параллельно для каждого   -  person M.kazem Akhgary    schedule 23.11.2015
comment
Попробуйте отключить отчет о прогрессе; все задачи могут застрять в ожидании потока пользовательского интерфейса. Кроме того, если все эти потоки используют дисковый ввод-вывод, они также могут застрять в ожидании доступа к диску.   -  person Emond Erno    schedule 23.11.2015
comment
Без хорошего, минимального, полного примера кода, надежно воспроизводящего проблему , невозможно будет сказать, что может происходить. Короткий ответ заключается в том, что нет... параллельная обработка по своей сути не приводит к заиканию в пользовательском интерфейсе. Но определенно есть способы, которые могут произойти в некоторых случаях. Если вам нужна полезная помощь по вашему конкретному сценарию, вам нужно предоставить хороший пример кода для работы с ним.   -  person Peter Duniho    schedule 23.11.2015
comment
Лучше сломать вместо исключения? Кажется, существуют смешанные рекомендации по использованию. Изначально я делал state.break();   -  person Matt P.    schedule 24.11.2015
comment
Я отключил отчетность и при перетаскивании формы нет заиканий, но при наведении на кнопку на подсветку кнопки уходит пара секунд. Отмена также занимает некоторое время. Я посмотрю, что я могу сделать для минимального полного примера кода.   -  person Matt P.    schedule 24.11.2015
comment
Хорошо... Я заменил код минимальным полным примером кода. Спасибо за помощь :)   -  person Matt P.    schedule 24.11.2015
comment
Вы не должны вызывать Directory.GetFiles или Directory.CreateDirectory в потоке пользовательского интерфейса, оба являются медленными операциями файловой системы.   -  person Ian Mercer    schedule 24.11.2015
comment
Не имеет отношения к ответу, но использование Image.FromStream предпочтительнее Image.FromFile. У последнего есть «проблемы». См. в другом месте на SO.   -  person Ian Mercer    schedule 24.11.2015
comment
Извините... только что сделал это для примера... фактическое приложение создает каталоги в цикле Parallel.Foreach, поскольку файлы находятся в разных подкаталогах. Также пути к файлам считываются из отдельного файла, который выполняется в отдельном потоке с помощью Task.Run.   -  person Matt P.    schedule 24.11.2015


Ответы (2)


Я считаю, что нашел проблему. GDI+ блокируется в потоке пользовательского интерфейса при вызове Graphics.DrawImage() в фоновом потоке. См. Почему графические операции выполняются в фоновом потоке блокировать графические операции в основном потоке пользовательского интерфейса?

Очевидным решением было бы использование нескольких процессов (см.: Распараллеливание GDI+ Image Resizing .net )

person Matt P.    schedule 01.12.2015

Я вижу здесь две потенциальные проблемы:

  1. Вы проверяете отмену в начале тела цикла, что не позволяет прерывать каждую итерацию цикла во время выполнения операции. Заикание после отмены, вероятно, связано с тем, что изменение размера изображения все еще выполняется. Вероятно, было бы лучше прервать поток (что не рекомендуется, но в этом случае это может работать быстрее).
  2. _CancelSource.Cancel() блокирует поток пользовательского интерфейса. Вы можете выполнить отмену как асинхронную задачу. Проверьте соответствующий пост: Почему отмена блокируется так долго при отмене большого количества HTTP-запросов?.

Что касается перегрузки процессора, то это тоже возможно. Вы можете использовать профилировщик для проверки использования процессора. Visual Studio имеет встроенный профилировщик, который очень хорошо работает с C#.

person Igor Ševo    schedule 25.11.2015
comment
Я попытался вызвать отмену в отдельном потоке, но это не имело значения, поэтому я думаю, что пауза вызвана заиканием, которое происходит перед отменой. Загрузка ЦП будет достигать 85-95%. - person Matt P.; 26.11.2015