一、GUI 程序中的异步操作
1、在 GUI 程序中使用异步操作
在 GUI程序中, 首先理解关于 UI 显示变化的概念。
- 消息: UI 上的行为,如点击按钮、展示标签、移动窗体等。
- 消息队列: 把要触发的所有消息,都按照相关的顺序放入到里面。
- 消息泵: 消息泵主要从队列里提取出并处理该消息。
- 处理程序代码: 与消息“绑定”的实现代码,如点击按钮消息,执行程序代码:btnDoStuff_Click。
因为在GUI 程序上对所有UI效果的变化都必须在主 GUI 线程中完成的,所以 UI主线程通过消息泵来对UI消息进行处理。
消息泵从队列中取出一条消息,并调用它的处理程序代码。当处理程序代码完成时,消息泵获取下一条消息并循环这个过程。
但是如果某个消息的处理程序代码耗时过长,消息队列中的消息会产生积压(消息没有及时进行移除并处理),程序将会一时失去响应,在等待当前处理程序代码处理完成之前。没法处理其他任何消息。
以下是WPF 应用程序中处理程序代码中使用了 Thread.Sleep 的示例:
xaml代码:
<StackPanel>
<Label Name="lbStatus" Margin="10,5,10,0">Not Doing Anything</Label>
<Button Name="btnDoStuff" Content="Do Stuff" HorizontalAlignment="Left" Margin="10,5" Padding="5,2" Click="btnDoStuff_Click"></Button>
</StackPanel>
后台cs代码:
private void btnDoStuff_Click(object sender, RoutedEventArgs e)
{
btnDoStuff.IsEnabled = false;
lbStatus.Content = "Doing Stuff";
Thread.Sleep(4000);
lbStatus.Content = "Not Doing Anything";
btnDoStuff.IsEnabled = true;
}
运行结果:
lbStatus 标签看起来没有改变,是因为还没有及时把刷新标签内容的消息进行处理,就直接暂停线程了。4秒后线程重新工作,这时其实是有把标签改变为 Doing Stuff 的,但是处理的时间太快,就立马执行下一条刷新的消息,所以肉眼上看不出内容改变的效果。如果把 lbStatus.Content = “Not Doing Anything”; 这条语句注释掉,就会发现4秒后标签就显示为 Doing Stuff 。
如果把 Thread.Sleep 改为Task.Delay,点击按钮后就不会卡顿。这是因为 Thread.Sleep 会阻塞线程,而 Task.Delay 不会阻塞线程(暂时不处理对应消息的处理程序代码),线程还可以继续处理其他工作。(其他触发的消息)
2、Task.Yield
Task.Yield 方法创建一个立即返回的 awaitable。当异步方法在处理程序代码里被调用时,异步方法里的 每次执行一个 await Task.Yield,就会把当前消息从消息队列中移除,再回到队列末尾,移交控制权给其他任务由处理器继续处理。
跟 Task.Delay 不同的一点就是:
- 调用一次 Task.Yield 就会把消息重新回到队列末尾中,重新在等到它时才会继续处理后续部分代码。
- 调用Task.Delay 设置一定时间内,先处理其他消息,等时间到了,才会继续处理它的后续部分代码。
使用异步 Lambda 表达式
XAML:
<StackPanel>
<TextBlock Name="workStartedTextBlock" Margin="10,10"/>
<Button Name="StartWorkButton" Width="100" Margin="4" Content="Start Work"/>
</StackPanel>
后台 CS 代码:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
//异步 Lambda 表达式:async (sender, e) =>
StartWorkButton.Click += async (sender, e) =>
{
SetGuiValues(false, "Work Started");
await DoSomeWork();
SetGuiValues(true, "Work Finished");
};
}
private void SetGuiValues(bool buttonEnabled,string status)
{
StartWorkButton.IsEnabled = buttonEnabled;
workStartedTextBlock.Text = status;
}
private Task DoSomeWork()
{
return Task.Delay(2500);
}
}
二、BackgroundWorker 类
1、BackgroundWorker 类
该类创建一个新线程,是属于后台线程,在后台持续运行以完成某项工作,并不时地与主线程通信。
类的主要成员:
属性:
- WorkerReportsProgress 和 WorkerSupportsCancellation: 这两个属性用于设置后台任务是否可以把它的进度汇报给主线程以及是否支持从主线程取消。
- IsBusy: 检查后台任务是否正在运行。
事件:
用于发送不同的程序事件和状态。
- DoWork: 在后台线程开始的时候触发。
- ProgressChanged: 在后台任务汇报进度的时候触发。
- RunWorkerCompleted: 在后台工作线程退出的时候触发。
方法:
用于开始行为或改变状态。
-
RunWorkerAsync: RunWorkerAsync 方法获取后台线程并且执行 DoWork 事件处理程序。
-
CancelAsync: 调用 CancelAsync 方法把 CancellationPending 属性设置为 true。DoWork 事件处理程序需要检查这个属性来决定是否应该停止处理。
-
DoWork: DoWork 事件处理程序(在后台线程)在希望向主线程汇报进度的时候,调用 ReportProgress 方法。
DoWork 是必需的,该事件跟事件处理程序关联,包含有在后台线程执行的代码。
ProgressChanged 和 RunWorkerCompleted 是可选的,取决于程序的需要。
后台线程处理程序的原理:
-
DoWork 事件包含后台线程上执行的代码。
- 主线程调用 BackgroundWorker 对象的 RunWorkerAsync 方法的时候会触发 DoWork 事件。
-
后台线程通过调用 ReportProgress 方法与主线程通信。届时将触发 ProgressChanged 事件,主线程可以附加到 ProgressChanged 事件上的处理程序处理事件。
-
附加到 RunWorkerCompleted 事件的处理程序应该包含在后台线程完成 DoWork 事件处理程序的执行之后需要执行的代码。(即结束该后台线程工作的最后一个事情)
主要事件的声明如下:
void DoWorkEventHandler(object sender, DoWorkEventArgs e)
void ProgressChangedEventHandler(object sender, ProgressChangedEventArgs e)
void RunWorkerCompletedEventHandler(object sender, RunWorkerCompletedEventArgs e)
2、配置 BackgroundWorker 类对象:
-
若需要工作线程向主线程汇报进度,把 WorkerReportsProgress 属性设置为 true。(启用的话,会调用 ReportProgress 方法,从而触发 ProgressChanged 事件。)
-
若从主线程取消工作线程,就把 WorkerSuppoortsCancellation 属性设置为 true。(启用的话,DoWork 事件处理程序代码会定期检查 CancellationPending 属性是否取消了。若是,则退出。)
类对象配置好后,调用 RunWorkerAsync 方法来启动它。
在 WPF 程序中使用 BackgroundWorker 类的示例:
XAML 代码:
<StackPanel>
<ProgressBar Name="progressBar" Height="20" Width="200" Margin="100"/>
<Button Name="btnProcess" Width="100" Click="btnProcess_Click"
Margin="5">Process</Button>
<Button Name="btnCancel" Width="100" Click="btnCancel_Click"
Margin="5">Cancel</Button>
</StackPanel>
后台 cs 代码:
public partial class MainWindow : Window
{
BackgroundWorker bgWorker = new BackgroundWorker();
public MainWindow()
{
InitializeComponent();
//设置 BackgroundWorker 属性
bgWorker.WorkerReportsProgress = true;
bgWorker.WorkerSupportsCancellation = true;
//连接 BackgroundWorker 对象的处理程序
bgWorker.DoWork += DoWork_Handler;
bgWorker.ProgressChanged += ProgressChanged_Handler;
bgWorker.RunWorkerCompleted += RunWorkerCompleted_Handler;
}
private void btnProcess_Click(object sender, RoutedEventArgs e)
{
if (!bgWorker.IsBusy)
bgWorker.RunWorkerAsync();
}
private void btnCancel_Click(object sender, RoutedEventArgs e)
{
bgWorker.CancelAsync();
}
private void ProgressChanged_Handler(object sender,ProgressChangedEventArgs args)
{
progressBar.Value = args.ProgressPercentage;
}
private void DoWork_Handler(object sender, DoWorkEventArgs args)
{
BackgroundWorker worker = sender as BackgroundWorker;
for(int i = 1; i <= 10;i++)
{
if(worker.CancellationPending)
{
args.Cancel = true;
break;
}
else
{
worker.ReportProgress(i * 10);
Thread.Sleep(500);//这里当前线程为后台线程
}
}
}
private void RunWorkerCompleted_Handler(object sender, RunWorkerCompletedEventArgs args)
{
progressBar.Value = 0;
if (args.Cancelled)
MessageBox.Show("Process was cancelled.", "Process Cancelled");
else
MessageBox.Show("Process completed normally.", "Process Completed");
}
}
三、并行循环
任务并行库是 BCL 中的一个类库。
如果迭代之间彼此独立,并且程序运行在多处理器机器上。
使用条件: 迭代之间彼此独立。每=次迭代运行的过程和结果都互相不影响彼此,如计算多个不同范围的面积。若每次迭代都叠加数值最终得出一个总数,则不适合使用并行循环。
1、Parallel.For:
public static ParallelLoopResult.For(int fromInclusive, int toExclusive, Action body);
- fromInclusive: 迭代系列的第一个整数。
- toExclusive: 比迭代系列最后一个索引号大1的整数。和使用表达式 index<ToExclusive 计算一样。
- body: 接受单个输入参数的委托,body 的代码在每一次迭代中执行一次。
class Program
{
static void Main()
{
Parallel.For(0,15,i =>
Console.WriteLine($"The square of{ i } is { i * i}"));
}
}
输出结果:
The square of 0 is 0
The square of 1 is 1
The square of 2 is 4
The square of 4 is 16
The square of 5 is 25
The square of 6 is 36
The square of 7 is 49
The square of 8 is 64
The square of 9 is 81
The square of 10 is 100
The square of 11 is 121
The square of 12 is 144
The square of 13 is 169
The square of 14 is 196
The square of 3 is 9
整数数组的示例:
class Program
{
static void Main()
{
const int maxValues = 50;
int[] squares = new int[maxValues];
Parallel.For(0,maxValues,i => squares[i] = i *i);
}
}
2、Parallel.ForEach
Parallel.ForEach 方法有相当多的重载,其中最简单的如下:
static ParallelLoopResult ForEach<TSource>(IEnumerable<TSource> source,Action<TSource> body)
示例:
class Program
{
static void Main(string[] args)
{
string[] squares = new string[]
{ "We","hold","these","truths","to","be","self-evident",
"that","all","men","are","created","equal"};
Parallel.ForEach(squares, s => Console.WriteLine(string.Format($"\"{ s }\" has { s.Length } letters")));
Console.ReadKey();
}
}
输出结果:
“We” has 2 letters
“truths” has 6 letters
“to” has 2 letters
“be” has 2 letters
“self-evident” has 12 letters
“that” has 4 letters
“all” has 3 letters
“men” has 3 letters
“are” has 3 letters
“created” has 7 letters
“equal” has 5 letters
“hold” has 4 letters
“these” has 5 letters