65.9K
CodeProject 正在变化。 阅读更多。
Home

WinForms 和 TPL - 实现快速多任务和响应式用户界面

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (16投票s)

2014年3月25日

CPOL

9分钟阅读

viewsIcon

51764

downloadIcon

2597

在 WinForms 中使用任务并行库 (TPL) 实现多任务和 UI 响应性

如果您遇到上述 codeproject 下载链接问题,请尝试下面的 skydrive 链接


源代码 - http://1drv.ms/OQNUYU
仅可执行文件 - http://1drv.ms/1l0wBj3

引言

好的。首先,本文绝不会描述任何超级先进的技术或任何新颖的东西。TPL 对许多开发人员来说仍然相对较新,当涉及到桌面应用程序中的多任务和 UI 响应性时,codeproject 上的大多数示例都倾向于用 WPF 演示 TPL。尽管在 WinForms 中也能实现相同的功能,但我尝试在这里编写一个 WinForms 示例。所以,我尝试演示 TPL 中实现多任务和更新 Windows 窗体用户界面是多么容易。考虑到 .Net 4.0,您应该熟悉 Action 委托。




本文将帮助您了解:

1. 新任务创建
2. UI 响应性和 UI 更新
3. 向任务传递数据
4. 任务链
5. 一个示例应用程序,助您开始第一个 WinForms 多任务 尝试

本文未解释的内容:

1. 任务取消操作
2. 等待任务完成
3. 数据共享的同步机制
4. 通过 IProgress 接口报告进度
5. 什么是任务?为什么是任务?
6. 什么是线程池?为什么不使用 QueueUserWorkItem?

必备组件

强制性
线程与任务,或者至少是 任务并行库:1 of n 的介绍和前几段

可选
在任务并行库和线程池之间进行选择


[简单来说,任务是一个将被安排在 .Net 线程池的线程上运行的方法。.Net 线程池会根据需要创建和管理线程。]

背景

在 TPL 之前,我一直使用 ThreadPool 的 QueueUserWorkItem InvokeRequiredInvoke 的组合。我一直无法真正掌握 WindowsFormsSynchronizationContext.Post 方法。因此,BackgroundWorkerQueueUserWorkItemInvokeRequiredInvoke 的组合确实是一个很好的模式,直到 TPL 出现,大大简化了事情。

在正常情况下,您可能会遇到以下情况:

  1. 您希望在特定任务完成后更新 UI;
  2. 或者您可以选择在任务达到检查点(代码中的某个位置,或达到业务逻辑后)或根据您的标准定义明确的更新点时更新 UI;
  3. 或者如果您持续轮询资源,您可能希望像资源轮询例程轮询资源一样频繁地更新 UI。
    以及更多类似的情况。
有几点需要记住:
  1. Task.Factory.StartNew() 接受一个方法作为参数,并将其安排为任务,在 .Net 线程池的可用线程之一上运行
  2. TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); 返回当前执行语句的线程的 TaskScheduler 实例。因此,当使用 UI 线程(或主线程)调用时,我们获得 UI 线程(或主线程)的 TaskScheduler 实例

注意

1. 您可能会看到多任务和多线程交替使用;尽管多线程指的是管理多个操作系统线程,而 .Net 术语中的多任务指的是管理多个 Task 实例。任务随后被安排在 .Net 线程上运行。
2. 如果您还没有在 WinForms 中进行过多线程操作,那么您必须知道创建 UI 的主线程(又称 UI 线程)是唯一允许更新 UI 的线程。这是一条严格的规则,如果您违反它,即,如果您尝试从另一个线程更新 UI,则会抛出 System.InvalidOperationException。在调试模式下(当调试器附加时),您肯定会收到此异常,但在调试器未附加时,您可能会或可能不会收到此异常。

示例应用程序

本文中提供的 WinForms 应用程序 - ResponsiveWinFormsWithTPL - 只是一个用户界面,其中包含按钮单击事件处理程序和一些其他方法,这些方法执行一些工作并更新 UI。 有四个 ProgressBar 对象和四个 Button 对象,它们彼此靠近。当您单击按钮时,应用程序将执行一些工作——实际上是 Thread.Sleep(int)——并将更新进度条及其附近的标签,同时保持 UI 响应。

单击按钮后,实际工作委托给另一个类中的方法(我们希望从中更新 UI),该方法实际执行任何指定操作的繁重工作。此外,大多数情况下,您的线程所做的工作位于其他类或 dll 的其他方法中。

在我们的示例中,BusinessLayer.cs 下的 BusinessLayer 类将演示任务实际完成的工作。这意味着实际的工作任务是从 BusinessLayer 的静态方法中派生出来的。当您想从 BusinessLayer 向 UI 报告进度时,您肯定需要某种回调机制。这里方便的是一个 Action<T> 委托,当我们需要将当前任务的进度更新回 UI 时,我们将从 BusinessLayer 触发它。因此请注意,BusinessLayer 类中的每个方法都将一个 action 委托作为第二个参数,通过它将进度更新回调用者。这实际上是一个常见的模式,任何希望不时向其调用者更新状态的方法都可以将 Action 委托作为参数。


BusinessLayer 类中虚构命名的四个静态方法如下:
  1. 名为 ProcessData 的方法将在任务完成后更新 UI。
  2. 名为 PerformInternalValidationsOnData 的方法将在完成一定量的任务后更新 UI。
  3. 名为 PrepareTransformationsForProcessing 的方法将以 15 毫秒的间隔非常频繁地更新 UI。
  4. 名为 PrepareLaunchSequenceForData 的方法将通过生成一个新任务来调用回调本身来更新 UI。最后一个方法还将演示向任务传递数据的正确方法,即以变量 state 的名义发送的变量 i

使用代码

因此,如前所述,如果您希望库中的某个方法向调用者报告状态,那么请为该方法添加另一个参数,例如 Action updateCaller,或 Action<string> updateCallerWithAMessage。当您想向调用者更新状态时,只需调用 updateCaller() 委托。然后由 updateCaller 来更新 UI 或记录日志或执行任何操作。

为了说明问题,让我们看一下 MainForm.cs 中的方法 private void button1_Click(object sender, EventArgs e),它调用 BusinessLayer 中名为 public static void ProcessData(int data, Action<string> updateUICallBack) 的方法。ProcessData 方法期望一个整数参数和一个接受字符串的委托(请参阅顶部解决方案资源管理器图像中的 ProcessData 方法签名,或底部的完整方法定义)。

        private void button1_Click(object sender, EventArgs e)
        {
            button1.Enabled = false;
            pictureBox1.Show();

            BusinessLayer.ProcessData(count, (message) => UpdateProgressBar1(message));
            ++count;
        }

MainForm.cs 中还有另一个名为 private void UpdateProgressBar1(string statusMessage) 的方法,它接受一个字符串参数。因此,在调用 BusinessLayer.ProcessData 时,我们将把 UpdateProgressBar1 方法作为参数传递给我们的 Action<string> 参数 updateUICallBack

        private void UpdateProgressBar1(string statusMessage)
        {
            Task.Factory.StartNew(() =>
                {
                    progressBar1.PerformStep();
                    label1.Text = statusMessage;
                    pictureBox1.Hide();
                    button1.Enabled = true;
                }, CancellationToken.None, TaskCreationOptions.None, uiScheduler);
        } 


显然,UpdateProgressBar1 方法使用创建和运行 Task 的典型语法(使用 Task.Factory.StartNew 方法)更新 UI 组件,例如 progressBar1、label1 等。 uiSchedulerStartNew 方法的第四个参数,它非常重要。 uiScheduler 的类型是 TaskScheduler,它在 MainForm_Load 方法中按如下方式赋值:

        private void MainForm_Load(object sender, EventArgs e)
        {
            uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
        }    


每次有 UI 更新时,安全的方法是生成一个带有分配了 UI 线程上下文的 TaskScheduler 实例的 Task。在 MainForm_Load 或构造函数 MainForm() 中将 TaskScheduler 分配为 TaskScheduler.FromCurrentSynchronizationContext ,保证返回一个来自 UI 线程同步上下文的 TaskScheduler 实例。

ProcessData 方法中,我们使用 Task.Factory.StartNew 启动一个新 Task,该 Task 睡眠 2000 毫秒,一旦该任务完成,它将继续(通过 ContinueWith)调用委托方法 updateUICallBack ,其参数将是 UpdateProgressBar1 方法。

        /// <summary>
        /// Method will update the UI once the task is entirely complete
        /// </summary>
        /// <param name="data"></param>
        /// <param name="updateUICallBack"></param>
        public static void ProcessData(int data, Action<string> updateUICallBack)
        {
            Task.Factory.StartNew(
                () =>
                {
                    Thread.Sleep(2000); //simulateWork, do something with the data received
                })
                .ContinueWith(
                (cw) =>
                {
                    updateUICallBack(string.Format("Finished step {0}", data));
                }
            );
        } 


所以,到目前为止我们已经有了:按钮点击事件调用库方法 ProcessDataProcessData 方法执行一些工作,并在工作完成后,它通过带有 uiScheduler 的新 Task 调用回调方法 UpdateProgressBar1 来更新 UI。

ContinueWith 任务只会在 StartNew 中的任务完成后运行。我没有计划解释创建任务的语法,但是请随意浏览整个解决方案,我确信您会理解的。

这是一个在任务完成时更新 UI 的示例。

如果您查看方法 PerformInternalValidationsOnData,它会在任务完成一定百分比后更新调用者。

        /// <summary>
        /// Method will update the UI while the task is running
        /// </summary>
        /// <param name="data"></param>
        /// <param name="updateUICallBack"></param>
        public static void PerformInternalValidationsOnData(object data, Action<string, bool> updateUICallBack)
        {
            Task.Factory.StartNew(
                () =>
                {
                    Thread.Sleep(10); //simulateWork, do something with the data received
                    updateUICallBack("Running validation 20%", false);
                    Thread.Sleep(1000); //simulateWork, do something with the data received
                    updateUICallBack("Running validation 40%", false);
                    Thread.Sleep(800); //simulateWork, do something with the data received
                    updateUICallBack("Running validation 60%", false);
                    Thread.Sleep(700); //simulateWork, do something with the data received
                    updateUICallBack("Running validation 80%", false);
                    Thread.Sleep(1000); //simulateWork, do something with the data received
                    updateUICallBack("Validations complete - 100%", true);
                });
        } 


BusinessClass 中还有另外两个方法,即 PrepareTransformationsForProcessing PrepareLaunchSequenceForData,以及 MainForm.cs 中的其他方法,例如 UpdateProgressBar2、UpdateProgressBar3 和 UpdateProgressBar4,请查看它们并修改或设计您的第一个 WinForms 与 TPL。暂时就这些。

向任务传递数据

PrepareLaunchSequenceForData 方法在任务内部创建了一个新任务来更新 UI。如果您仔细观察,当我们通过 StartNew 调用 updateUICallBack 时,我们传递 i 作为参数,并且我们的 Task 接受一个名为 state 的新参数。而 state 在我们的任务内部使用。每当您想向任务传递数据时,您都必须将其作为参数传递给 StartNew 并在新 Task 内部访问它。如果您取消注释已注释的任务代码并注释带有参数 state 的任务,那么您实际上会看到 UI 中的差异。最后一个状态栏有时会显示 101% 完成,而不是 100% 完成。

        /// <summary>
        /// Method will update UI very frequently with new task while the original task is running
        /// </summary>
        /// <param name="data"></param>
        /// <param name="updateUICallBack"></param>
        public static void PrepareLaunchSequenceForData(object data, Action<string, bool> updateUICallBack)
        {
            Random rand = new Random();
            Task.Factory.StartNew(
                () =>
                {
                    for (int i = 0; i <= 100; i++)
                    {
                        Thread.Sleep(rand.Next(10, 11)); //simulateWork, do something with the data received

                        Task.Factory.StartNew(
                            (state) =>
                            {
                                updateUICallBack(string.Format("Preparing launch requence for data {0}% done", state), false);
                            }, i);

                        //Incorrect way of passing data to a task. 
                        //
                        //The value of variable i may be different from the time the task was scheduled
                        //and from the time the task actually runs
                        //
                        //If you comment the above task, and run the below task, sometimes you may see 101% done in the UI, instead of 100% done.
                        //
                        //Task.Factory.StartNew(
                        //   () =>
                        //   {
                        //       updateUICallBack(string.Format("Preparing launch requence for data {0}% done", i), false);
                        //   });
                    }

                        Task.Factory.StartNew(
                                () =>
                                {
                                    updateUICallBack("Preparing launch requence for data 100% complete", true);
                                });
                    
                });
        } 

关注点

本文的宗旨是为需要在 WinForms 中使用响应式多线程应用程序的人演示并提供一个入门示例。这也是一个非常基础的示例。如果您喜欢本文的思路,您可能还会喜欢阅读:

  1. 同步和异步委托类型
  2. 线程池与任务, 线程池与任务
  3. .NET UI 上下文中的异步
  4. "Task.Factory.StartNew" 与 "new Task(...).Start"
  5. 在 WPF 中使用任务并行库 (TPL)
  6. 任务并行库:6/n

历史

版本 1:首次发布
版本 2:在“兴趣点”部分添加了先决条件和必要链接

© . All rights reserved.