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){
    ...;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值