参考书:《 visual C# 从入门到精通》
第四部分 用C#构建UMP应用
第23章 使用任务提高吞吐量
文章目录
23.1 使用并行处理执行多任务处理
在应用程序中执行多任务处理主要是因为:
- 增强可响应性
- 增强可伸缩性
23.2 用 .NET Framework 实现多任务处理
Microsoft在System.Threading.Tasks
命名空间中提供了Task
类和一套相关的类型用于解决实现多任务处理会遇到的一些问题。我们需要考虑的主要是如何将应用程序分解成一组并发操作。
23.2.1 任务、线程和线程池
Task
类是对一个并发操作的抽象。要创建Task对象来运行一个代码块。可实例化多个Task
对象。如果有足够数量的处理器,就可以让他们并发运行。
WinRT
内部使用Thread
对象和ThreadPool
类实现任务并调度它们的执行。
Task
类对线程处理进行了强大抽像,使你可以简单的区分应用程序的并行度和并行单位。
CLR
对实现一组并发任务所需的线程数量进行了优化,并根据可用的处理器数量调度它们。
我们的代码需要做的是将应用程序分解成可并行运行的任务。WinRT
根据处理器和计算器的工作复合创建适当数量的线程,将你的任务和这些线程关联,并安排它们高效运行。
23.2.2 创建、运行和控制任务
Task
构造器有多个重载版本,所有版本都要求提供一个Action
委托作为参数。Action
委托引用不返回值的方法。如:
Task task=new Task(dowork);
...;
private void dowork(){
...;
}
Task
的其它构造器的重载版本要求获取一个Action<object>
参数,如:
Action<object> action;
action=doWorkWithObject;
object parameterData=...;
Task task=new Task(action parameterData);
...;
private void doWorkWithObject(object o){
...;
}
创建好Task
对象后可以用Start
方法来启动:
Task task=new Task(...);
task.Start();
Task类提供了静态方法Run
同时实现创建和运行操作,它获取相关的Action委托,立即开始任务并返回对Task对象的引用:Task task=Task.Run(()=>dowork());
任务运行的方法结束后任务就会结束,运行任务的线程会返回线程池。
可以安排在一个任务结束后执行另一个任务。延续用Task对象的ContinueWith
方法创建。这样一个Task对象的操作完成后,调度器自动创建新Task对象来运行由ContinueWith方法指定的操作。延续所指定的方法需要获取一个Task参数,调度器向方法传递对已完成任务的引用。ContinueWith返回一个新的Task对象的引用。
Task task=new Task(dowork);
task.Start();
Task newTask=task.ContinueWith(doMoreWork);
...;
private void dowork(){
...;
}
...;
private void doMoreWork(Task task){
...;
}
ContinueWith方法由很多重载版本,通过参数来指定额外的项。
如,为任务添加延续任务,只有在初始操作没有抛出未处理异常的情况下才运行延续任务:
Task task=new Task(doWork);
task.ContinueWith(doMoreWork,TaskContinuationOptions.NotOnFaulted);
task.Start();
Task类提供了Wait
方法,实现简单的任务协作机制,它允许阻塞(暂停)当前的进程至指定的任务完成:
Task task2=...;
task2.Start();
...;
task2.Wait();
可用Task类的静态方法WaitAll
和WaitAny
等待一组任务。WaitAll等待指定的所有方法都完成,而WaitAny等待指定的至少一个任务完成。
Task.WaitAll(task,task2);
TaskWaitAny(task,task2);
23.2.3 使用Task类实现并行处理
现在我们可以进行实战。新建一个UWP应用。在设计视图中拖入四个空间:两个TextBlock(其中一个命名为duration
),一个Button,一个Image命名为graphImage
。布局的源文件为:
MainPage.xaml
<Page
x:Class="C_23_2_3.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:C_23_2_3"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid>
<Image x:Name="graphImage" HorizontalAlignment="Left" Height="700" Margin="274,171,0,0" VerticalAlignment="Top" Width="940"/>
<Button x:Name="plotButton" Content="Plot Graph" Margin="54,379,0,0" VerticalAlignment="Top" Height="66" Width="191" FontSize="22" Click="plotButtonClick"/>
<TextBlock x:Name="duration" HorizontalAlignment="Left" Margin="74,567,0,0" Text="" TextWrapping="Wrap" VerticalAlignment="Top" Height="76" Width="148" FontSize="22"/>
<TextBlock Text="Graph Demo" TextWrapping="Wrap" Margin="609,84,555,825" FontSize="36"/>
</Grid>
</Page>
MainPage.xaml.cs
的代码:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Navigation;
using System.Threading.Tasks;
// https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x804 上介绍了“空白页”项模板
namespace C_23_2_3
{
/// <summary>
/// 可用于自身或导航至 Frame 内部的空白页。
/// </summary>
public sealed partial class MainPage : Page
{
private int pixelWidth = 15000/2;
private int pixelHeight = 10000/2;
private int bytesPerpixel = 4;
private WriteableBitmap graphBitmap = null;
private byte[] data;
private byte redValue, greenValue, blueValue;
public MainPage()
{
this.InitializeComponent();
int dataSize = bytesPerpixel * pixelHeight * pixelWidth;
data = new byte[dataSize];
graphBitmap = new WriteableBitmap(pixelWidth, pixelHeight);
}
private void plotButtonClick(object sender, RoutedEventArgs e)
{
Random rand = new Random();
redValue = (byte)rand.Next(0xFF);
greenValue = (byte)rand.Next(0xFF);
blueValue = (byte)rand.Next(0xFF);
Stopwatch watch = Stopwatch.StartNew();
//generateGraphData(data);
Task first = Task.Run(() => generateGraphData(data, 0, pixelWidth / 8));
Task second = Task.Run(() => generateGraphData(data, pixelWidth / 8, pixelWidth / 4));
Task Third= Task.Run(() => generateGraphData(data, pixelWidth/4, pixelWidth *3/ 8));
Task forth= Task.Run(() => generateGraphData(data, pixelWidth*3 / 8,pixelWidth/2));
Task.WaitAll(first, second,Third,forth);
duration.Text = $"Duration (ms):{watch.ElapsedMilliseconds}";
Stream pixelStreem = graphBitmap.PixelBuffer.AsStream();
pixelStreem.Seek(0, SeekOrigin.Begin);
pixelStreem.Write(data, 0, data.Length);
graphBitmap.Invalidate();
graphImage.Source = graphBitmap;
}
private void generateGraphData(byte[] data,int partitionStart,int partitionEnd)
{
int a = pixelWidth / 2;
int b = a * a;
int c = pixelHeight / 2;
for(int x = partitionStart; x <partitionEnd; ++x)
{
int s = x * x;
double p = Math.Sqrt(b - s);
for(double i = -p; i < p; i += 3)
{
double r = Math.Sqrt(s + i * i) / a;
double q = (r - 1) * Math.Sin(24 * r);
double y = i / 3 + (q * c);
plotXY(data, (int)(-x + (pixelWidth / 2)), (int)(y + (pixelHeight / 2)));
plotXY(data, (int)(x + (pixelWidth / 2)), (int)(y + (pixelHeight / 2)));
}
}
}
private void plotXY(byte[] data, int v1, int v2)
{
int pixelIndex=(v1+v2*pixelWidth)*bytesPerpixel;
data[pixelIndex] = blueValue;
data[pixelIndex + 1] = greenValue;
data[pixelIndex + 2] = redValue;
data[pixelIndex + 3] = 0xBF;
}
}
}
更多的分析可以参见《Visual c# 从入门到精通》。
运行结果如下:
23.2.4 使用Parallel类为任务进行抽象
Parallel类允许对常见编程构造进行“并行化”,同时不要求重新设计应用程序。在内部Parallerl类会创建他自己的一组Task对象,并在这些对象完成时自动同步。Parallel类在System.Threading.Tasks
命名空间中定义,它提供如下的一些静态方法:
Parallel.For
:用该方法代替For语句。在它定义的循环中,迭代可用任务来并行运行。它有很多重载版本。需要指定起始值和结束值以及一个方法引用,该方法获取一个整数参数。例如,对于一个简单的for循环:
for(int x =0;x<100;++x){
...;
}
由于不知道循环主体执行的操作是什么,但可以假设在某些情况下,我们你能将这个循环替换成一个Parallel.For构造,使它以并行方式执行迭代:
Parallel.For(0,100,performLoopProcessing);
...;
private void performLoopProcessing(int x){
...;
}
Parallel.ForEach<T>
:类似的可以用它来代替foreach
语句Parallel.Invoke
:以并行任务的形式执行一组无参方法。要指定一组无参且无返回值的一组委托方法调用。每个方法调用都可以在单独的线程上以任何顺序运行。比如:Parallel.Invoke(dowork,doMoreWork,doYetMoreWork);
现在我们修改一下前面的应用程序,MainPage.xaml.cs
的代码:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Navigation;
using System.Threading.Tasks;
// https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x804 上介绍了“空白页”项模板
namespace C_23_2_3
{
/// <summary>
/// 可用于自身或导航至 Frame 内部的空白页。
/// </summary>
public sealed partial class MainPage : Page
{
private int pixelWidth = 15000/2;
private int pixelHeight = 10000/2;
private int bytesPerpixel = 4;
private WriteableBitmap graphBitmap = null;
private byte[] data;
private byte redValue, greenValue, blueValue;
public MainPage()
{
this.InitializeComponent();
int dataSize = bytesPerpixel * pixelHeight * pixelWidth;
data = new byte[dataSize];
graphBitmap = new WriteableBitmap(pixelWidth, pixelHeight);
}
private void plotButtonClick(object sender, RoutedEventArgs e)
{
Random rand = new Random();
redValue = (byte)rand.Next(0xFF);
greenValue = (byte)rand.Next(0xFF);
blueValue = (byte)rand.Next(0xFF);
Stopwatch watch = Stopwatch.StartNew();
generateGraphData(data);
/*
Task first = Task.Run(() => generateGraphData(data, 0, pixelWidth / 8));
Task second = Task.Run(() => generateGraphData(data, pixelWidth / 8, pixelWidth / 4));
Task Third= Task.Run(() => generateGraphData(data, pixelWidth/4, pixelWidth *3/ 8));
Task forth= Task.Run(() => generateGraphData(data, pixelWidth*3 / 8,pixelWidth/2));
Task.WaitAll(first, second,Third,forth);*/
duration.Text = $"Duration (ms):{watch.ElapsedMilliseconds}";
Stream pixelStreem = graphBitmap.PixelBuffer.AsStream();
pixelStreem.Seek(0, SeekOrigin.Begin);
pixelStreem.Write(data, 0, data.Length);
graphBitmap.Invalidate();
graphImage.Source = graphBitmap;
}
private void generateGraphData(byte[] data)
{
Parallel.For(0, pixelWidth / 2, x => calculateData(x, data));
}
private void calculateData(int x, byte[] data)
{
int a = pixelWidth / 2;
int b = a * a;
int c = pixelHeight / 2;
int s = x * x;
double p = Math.Sqrt(b - s);
for (double i = -p; i < p; i += 3)
{
double r = Math.Sqrt(s + i * i) / a;
double q = (r - 1) * Math.Sin(24 * r);
double y = i / 3 + (q * c);
plotXY(data, (int)(-x + (pixelWidth / 2)), (int)(y + (pixelHeight / 2)));
plotXY(data, (int)(x + (pixelWidth / 2)), (int)(y + (pixelHeight / 2)));
}
}
private void plotXY(byte[] data, int v1, int v2)
{
int pixelIndex=(v1+v2*pixelWidth)*bytesPerpixel;
data[pixelIndex] = blueValue;
data[pixelIndex + 1] = greenValue;
data[pixelIndex + 2] = redValue;
data[pixelIndex + 3] = 0xBF;
}
}
}
23.2.5 什么时候不要使用Parallel类
很多时候必须要根据实际情况来,有些时候并行化并不一定能提升性能。一般只在必要的时候才使用Parallel.Invoke
,在计算密集型的操作时适合用,其他时候用它的话创建和管理任务的开销反而会托类应用程序。
另外使用Parallel
类要注意并行操作一定要独立,相互迭代的操作之间不能有依赖,否则会出现逻辑错误。
23.3 取消任务和处理异常
可以使用Task类实现的协作式取消,让任务在方便时停止处理,并允许它在必要时撤销之前的工作。
23.3.1 协作式取消的原理
协作式取消基于取消标志。下面展示如何创建取消标志并用它取消任务:
public class MyApplication{
...;
private void initiateTasks(){
CancellationTokenSource cancellationTokenSource=new CancellationTokenSource();
CancellationToken cancellationToken=cancellationTokenSource.Token;
Task myTask=Task.Run(()=>doWork(cancellationToken));
...;
if(...){//指定在什么情况下取消任务
cancellationTokenSource.Cancel();
}
}
private void doWork(CancellationToken token){
...;
//若应用程序设置了取消标志,就结束处理
if(token.isCancellationRequested){
...;
return;
}
//没有被取消就继续执行
...;
}
}
还可以用Register
方法向取消标志登记一个回调方法(以Action委托的形式)。程序调用CancellationTokenSource对象的Cancel方法时会运行该回调。但不保证该方法什么时候运行。
...;
cancellationToken.Register(doAdditionalWork);
...;
private void doAdditionalWork(){
...;
}
我们同样的利用前面的应用程序为例,首先添加一个Button空间用来执行Cancel操作。
源程序MainPain,xaml.cs
的代码变为:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Navigation;
using System.Threading.Tasks;
using System.Threading;
// https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x804 上介绍了“空白页”项模板
namespace C_23_2_3
{
/// <summary>
/// 可用于自身或导航至 Frame 内部的空白页。
/// </summary>
public sealed partial class MainPage : Page
{
private int pixelWidth = 15000/2;
private int pixelHeight = 10000/2;
private int bytesPerpixel = 4;
private WriteableBitmap graphBitmap = null;
private byte[] data;
private byte redValue, greenValue, blueValue;
private Task first, second, third, fourth;
private void cancelClick(object sender, RoutedEventArgs e)
{
if (tokenSource != null)
tokenSource.Cancel();
}
private CancellationTokenSource tokenSource = null;
public MainPage()
{
this.InitializeComponent();
int dataSize = bytesPerpixel * pixelHeight * pixelWidth;
data = new byte[dataSize];
graphBitmap = new WriteableBitmap(pixelWidth, pixelHeight);
}
private async void plotButtonClick(object sender, RoutedEventArgs e)
{
Random rand = new Random();
redValue = (byte)rand.Next(0xFF);
greenValue = (byte)rand.Next(0xFF);
blueValue = (byte)rand.Next(0xFF);
tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
Stopwatch watch = Stopwatch.StartNew();
//generateGraphData(data);
first = Task.Run(() => generateGraphData(data, 0, pixelWidth / 4,token));
second = Task.Run(() => generateGraphData(data, pixelWidth / 4, pixelWidth / 2,token));
//third= Task.Run(() => generateGraphData(data, pixelWidth/4, pixelWidth *3/ 8,token));
//fourth= Task.Run(() => generateGraphData(data, pixelWidth*3 / 8,pixelWidth/2,token));
//Task.WaitAll(first, second,third,fourth);
await first;
await second;
/*这里不能用WaitAll,WaitAll方法会等任务完成的,这样就没法执行取消操作了
* 只有在标记为 async 的方法中才能使用await操作符,作用是释放当前的线程,
* 等待一个任务在后台完成。任务完成后,控制会回到方法中,从下一个语句继续。
*/
duration.Text = $"Duration (ms):{watch.ElapsedMilliseconds}";
Stream pixelStreem = graphBitmap.PixelBuffer.AsStream();
pixelStreem.Seek(0, SeekOrigin.Begin);
pixelStreem.Write(data, 0, data.Length);
graphBitmap.Invalidate();
graphImage.Source = graphBitmap;
}
private void generateGraphData(byte[] data, int partitionStart, int partitionEnd, CancellationToken token)
{
int a = pixelWidth / 2;
int b = a * a;
int c = pixelHeight / 2;
for (int x = partitionStart; x < partitionEnd; ++x)
{
int s = x * x;
double p = Math.Sqrt(b - s);
for (double i = -p; i < p; i += 3)
{
if (token.IsCancellationRequested)
return;
double r = Math.Sqrt(s + i * i) / a;
double q = (r - 1) * Math.Sin(24 * r);
double y = i / 3 + (q * c);
plotXY(data, (int)(-x + (pixelWidth / 2)), (int)(y + (pixelHeight / 2)));
plotXY(data, (int)(x + (pixelWidth / 2)), (int)(y + (pixelHeight / 2)));
}
}
}
private void plotXY(byte[] data, int v1, int v2)
{
int pixelIndex=(v1+v2*pixelWidth)*bytesPerpixel;
data[pixelIndex] = blueValue;
data[pixelIndex + 1] = greenValue;
data[pixelIndex + 2] = redValue;
data[pixelIndex + 3] = 0xBF;
}
}
}
运行结果:
点击上面的按钮,如果手速够快的话马上点击下面的按钮,这时会发现图标会出现一些空洞,这时生成的数据是不完整的。
可以检查Task对象的Status属性来了解一个任务是否成功完成。Status属性包含一个System.Threading.Tasks.TaskStatucs
枚举值,常用的有下面的:
- Created:任务的初始状态,表明任务以创建但尚未调度
- WaitingToRun:任务已调度但未开始运行
- Running:任务正在由一个线程运行
- RanToCompletion:任务成功完成,未发生任何我未处理异常
- Canceled:任务在开始运行前取消,或中途得得体地取消,为抛出异常
- Faulted:任务因异常而终止
23.3.2 为Canceled 和Faulted任务的使用延续
用ContinueWith方法并传递适当的TaskContinuationOption值,可以在任务被取消或抛出未处理异常时执行额外的工作。如:
Task task=new Task(doWork);
task.ContinueWith(doCancellationWork,TaskContinuationOptions.OnlyOnCanceled);
task.Start();
...;
private void doWork(){
...;
}
...;
private void doCancellationWork(Task task){
...;
}