并行处理使UI结结巴巴是正常的吗

本文关键字:UI 结结巴巴 并行处理 | 更新日期: 2023-09-27 18:23:51

我有一个Windows窗体应用程序,它可以复制GEDCOM谱系文件中的图像引用并重新调整其大小。用户从主窗体中选择文件和输出目录以及重新调整大小的选项,然后主窗体会以包含标签、进度条和按钮的对话框形式打开另一个窗体。我正在更新应用程序以使用.NET4.5中的新异步功能,并对其进行修改以使用并行处理。一切都很好,只是我注意到UI的响应有点不稳定(断断续续);如果我不使用百分比更新消息标签,那么它会更平滑。此外,当我取消任务时,UI将挂起1到15秒。这个应用程序只是供我个人使用的,所以没什么大不了的,但我很好奇是什么原因导致了这个问题,以及建议的处理方法是什么。并行处理只是因为有太多线程要处理而使CPU过载吗?我试着在每个循环迭代中添加一个Thread.Sleep(100),这似乎有点帮助。

以下是仍然会导致问题的应用程序的最低版本。复制:

  1. 使用以下表单创建新的windows表单应用程序
  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;
    }
}

并行处理使UI结结巴巴是正常的吗

我相信我发现了问题。在后台线程中调用Graphics.DrawImage()时,GDI+在UI线程中被阻止。请参阅为什么后台线程中的图形操作会阻止主UI线程中的图像操作?

一个明显的解决方案是使用多个进程(请参阅:并行GDI+Image Resizing.net)

我可以在这里看到两个潜在的问题:

  1. 您在循环体的开头检查是否取消,这不允许在执行操作时中断每个循环迭代。取消后的停顿可能是由于图像大小调整仍在执行。中止线程可能会更好(不建议这样做,但在这种情况下,它可能工作得更快)
  2. _CancelSource.Cancel()正在阻塞UI线程。您可以将取消操作作为异步任务。查看相关帖子:为什么在取消大量HTTP请求时,取消会阻止这么长时间

至于CPU过载,这也是可能的。您可以使用探查器来检查CPU使用情况。Visual Studio有一个集成的探查器,可以很好地与C#配合使用。