WinForm在Parallel.ForEach中调用Invoke导致程序无响应问题

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

namespace DataParallelismWithForEach
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();

            this.txtbox.Text += string.Format("Main Thread:{0}", Thread.CurrentThread.ManagedThreadId);
        }

        private void btnProcessImg_Click(object sender, EventArgs e)
        {
            // 获取文件夹下所有jpg图片
            string[] files = Directory.GetFiles(@"C:DesktopMyProjectOpenCV
esfor_csharp", "*.jpg", SearchOption.AllDirectories);
            // 创建新的文件夹
            string newDir = @"C:DesktopMyProjectOpenCV
esmodified_by_csharp";
            Directory.CreateDirectory(newDir);

#if false
            // 以阻塞方式处理图片
            foreach (string currentFile in files)
            {
                string fileName = Path.GetFileName(currentFile); // 根据全路径获取文件名

                using (Bitmap bmp = new Bitmap(currentFile))
                {
                    bmp.RotateFlip(RotateFlipType.Rotate180FlipNone); // 图片旋转180°
                    bmp.Save(Path.Combine(newDir, fileName));

                    this.txtbox.Text += string.Format("
Processing {0} on thread {1}", fileName, Thread.CurrentThread.ManagedThreadId);
                }
            }
#endif

#if false
            // 以下代码发现执行可能会不符合预期,有时执行一部分后,整个程序会冻结无响应
            // 网上找到的原因如下:
            /*
            先明确以下几点:
            1.Parallel.ForEach会阻塞直到所有任务完成或任务取消,但你采用的重载不支持取消,故它会阻塞直到任务完成。
            2.你的代码显示这个ForEach运行在窗体线程,即任务完成之前窗体线程将处于阻塞状态。
            3.this.Invoke会请求窗体线程执行委托,并在委托完成前阻塞。如果调用Invoke的线程即为窗体线程,则直接在当前线程执行委托并返回。

            综上,你的代码可能产生以下状况:
            1.事件触发,事件处理程序开始
            2.ForEach使用默认的任务调度器首先在当前线程(窗体线程)安排任务,并根据负载与等待情况安排其他工作线程参与执行任务。
            3a.如果单个任务执行非常快以致于创建线程的开销远大于在当前线程执行剩余任务的开销,那么任务调度器不会创建其它工作线程,所有的Invoke都在当前线程得以执行,ForEach返回。
            3b.如果创建线程可能加快任务处理速度,任务调度器会创建工作线程,并在工作线程上安排任务。
            4b.在工作线程上执行的Invoke全部被阻塞,等待窗体线程进行处理。
            5b.窗体线程执行所有可执行的任务后,等待ForEach返回,但由于其他线程在等待窗体线程处理Invoke,任务不能完成,ForEach无法返回,造成死锁。

            在调试程序时,如果出现死锁的情况,可以立刻中断,并打开线程窗口(调试->窗口->线程)查看各个线程在何处发生阻塞
                */
            // 并行方式
            Parallel.ForEach(files, (string currentFile)=>
            {
                string fileName = Path.GetFileName(currentFile); // 根据全路径获取文件名

                using (Bitmap bmp = new Bitmap(currentFile))
                {
                    bmp.RotateFlip(RotateFlipType.Rotate180FlipNone); // 图片旋转180°
                    bmp.Save(Path.Combine(newDir, fileName));

                    // 此时不能直接操作UI控件
                    //this.Text = string.Format("Processing {0} on thread {1}", fileName, Thread.CurrentThread.ManagedThreadId);

                    // 在MainForm对象上调用,允许子线程以线程安全方式访问控件
                    int tid = Thread.CurrentThread.ManagedThreadId;

                    this.Invoke(new Action(() =>
                    {
                        this.txtbox.Text += string.Format("
Processing {0} on thread {1}", fileName, tid);
                    }));
                }
            }
            );
#endif


#if true
            // 上面问题的解决方案:
            // 利用线程池安排一个线程执行并行循环
            System.Threading.ThreadPool.QueueUserWorkItem(obj =>
            {
                int subThreadId = Thread.CurrentThread.ManagedThreadId;
                this.Invoke(new Action(() =>
                {
                    this.txtbox.Text += string.Format("
Sub Thread Id:{0}", subThreadId);
                }));
                    
                Parallel.ForEach(files, (string currentFile) =>
                {
                    string fileName = Path.GetFileName(currentFile); // 根据全路径获取文件名

                    using (Bitmap bmp = new Bitmap(currentFile))
                    {
                        bmp.RotateFlip(RotateFlipType.Rotate180FlipNone); // 图片旋转180°
                        bmp.Save(Path.Combine(newDir, fileName));
                        int tid = Thread.CurrentThread.ManagedThreadId;

                        this.Invoke(new Action(() =>
                        {
                           this.txtbox.Text += string.Format("
Processing {0} on thread {1}", fileName, tid);
                        }));
                    }
                });
            });
#endif
        }
    }
}
原文地址:https://www.cnblogs.com/djh5520/p/14455630.html