WinForms 和 TPL - 实现快速多任务和响应式用户界面
在 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、 InvokeRequired 和 Invoke 的组合。我一直无法真正掌握 WindowsFormsSynchronizationContext.Post 方法。因此,BackgroundWorker 或 QueueUserWorkItem、InvokeRequired 和 Invoke 的组合确实是一个很好的模式,直到 TPL 出现,大大简化了事情。
在正常情况下,您可能会遇到以下情况:
- 您希望在特定任务完成后更新 UI;
- 或者您可以选择在任务达到检查点(代码中的某个位置,或达到业务逻辑后)或根据您的标准定义明确的更新点时更新 UI;
- 或者如果您持续轮询资源,您可能希望像资源轮询例程轮询资源一样频繁地更新 UI。
以及更多类似的情况。
Task.Factory.StartNew()
接受一个方法作为参数,并将其安排为任务,在 .Net 线程池的可用线程之一上运行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 委托作为参数。
- 名为
ProcessData
的方法将在任务完成后更新 UI。 - 名为
PerformInternalValidationsOnData
的方法将在完成一定量的任务后更新 UI。 - 名为
PrepareTransformationsForProcessing
的方法将以 15 毫秒的间隔非常频繁地更新 UI。 - 名为
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 等。 uiScheduler
是 StartNew 方法的第四个参数,它非常重要。 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));
}
);
}
所以,到目前为止我们已经有了:按钮点击事件调用库方法 ProcessData。ProcessData 方法执行一些工作,并在工作完成后,它通过带有 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 中使用响应式多线程应用程序的人演示并提供一个入门示例。这也是一个非常基础的示例。如果您喜欢本文的思路,您可能还会喜欢阅读:
- 同步和异步委托类型
- 线程池与任务, 线程池与任务
- .NET UI 上下文中的异步
- "Task.Factory.StartNew" 与 "new Task(...).Start"
- 在 WPF 中使用任务并行库 (TPL)
- 任务并行库:6/n
历史
版本 1:首次发布
版本 2:在“兴趣点”部分添加了先决条件和必要链接