visual C#(二十三)使用任務提高吞吐量

參考書:《 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類的靜態方法WaitAllWaitAny等待一組任務。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){
    ...;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章