參考書:《 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){
...;
}