visual C#(二十四)通过异步操作提高响应速度

参考书:《 visual C# 从入门到精通》
第四部分 用C#构建UMP应用
第 24 章 通过异步操作提高响应速度

文章目录

    • 24.1 实现异步方法
      • 24.1.1 定义异步方法:问题
      • 24.1.2 定义异步方法:解决方案
      • 24.1.3 定义返回值的异步方法
      • 24.1.4 异步方法注意事项
      • 24.1.5 异步方法和Windows Runtime API
    • 24.2 用PLINQ进行并行数据访问
      • 24.2.1 用PLINQ增强遍历集合时的性能
      • 24.2.2 取消PLINQ查询
    • 24.3 同步对数据的并发访问
      • 24.3.1 锁定数据
      • 24.3.2 用于协调任务的同步基元
      • 24.3.3 取消同步
      • 24.3.4 并发集合类
      • 24.3.5 使用并发集合和锁实现线程安全的数据访问

24.1 实现异步方法

异步方法是不阻塞当前执行线程的方法。

24.1.1 定义异步方法:问题

考虑一个问题,假定我们定义一系列很耗时的操作,我们要让这一些列操作依次执行,然后在一个TextBox上显示信息。我们也许会想到这样做:

private void slowMethod(){
    Task task=new task(first);
    task.ContinueWith(second);
    task.ContinueWith(third);
    task.Start();
    message.Text="Processing Completed";
}
private void first(){
    ...;
}
private void second(Task t){
    ...;
}
private void third(Task t){
    ...;
}

上述代码存在一些问题,一个问题是second和third方法的签名需要修改。一个更重要的问题是,这样做的话Start方法虽然发起了一个Task,但不会等它完成消息就会显示出来。所以应该修改:

private void slowMethod(){
    Task task=new Task(first);
    task.ContinueWith(second);
    task.ContinueWith(third);
    task.ContinueWith((t)->message.Text="Processing Complete");
    task.Start();
}

这样做的话又会导致另一个问题。调试模式运行上述代码的话最后一个延续会生成System.Exception异常,并显示消息:“应用程序调用了一个以为另一个线程整理的接口”。问题是只有UI线程才能处理UI控件,而它是企图从不同线程向TextBox控件写入。解决方案是使用Dispatcher对象,它是UI基础结构的组件,可调用其RunAsync方法请求在UI线程上执行操作。RunAsync方法获取一个Action委托来执行要运行的代码。

private void slowMethond(){
    Task task=new Task(first);
    task.ContinueWith(second);
    task.ContinueWith(third);
    task.ContinueWith((t)=>this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,()=>message.Text="Processing Complete"));
    task.Start();
}

24.1.2 定义异步方法:解决方案

定义异步方法要用到关键字asyncawaitasync指出方法含有可能要异步执行的操作,而await指定执行异步操作的地点。如下:

private async void slowMethod(){
    await first();
    await second();
    await thrid();
    messages.Text="Processing Complete";
} 

编译器在async方法中遇到await后会将操作符后面对 操作数重新格式化为任务,该任务在和async方法一样的线程上运行。剩余代码转换成延续,在任务完成后运行,而且是在相同线程上运行。由于运行async方法的线程是UI线程,直接访问窗口的控件也是没问题了。这样就不用通过Dispatcher对象了。

注意:

  • async执行方法中的代码可以分解成一个或多个延续,这些延续和原始方法调用在同一个线程上运行
  • await指定编译器在什么地方将代码分解成延续。await本身要求操作数是可等待对象,即提供了GetAwaiter方法的对象,该方法返回一个对象,它提供要运行并等待其完成的代码。编译器将你的代码转换成使用了这些方法的语句来创建相当的延续

在await操作符当前的实现中,作为操作数的可等待对象通常是一个Task。

例如,我们前面第23章的应用程序的代码可以修改为:

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;
using System.Threading;

// https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x804 上介绍了“空白页”项模板

namespace C_23_2_3
{
    /// 
    /// 可用于自身或导航至 Frame 内部的空白页。
    /// 
    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);
            try
            {
                await gen(data, 0, pixelWidth / 2, token);
                duration.Text = $"Duration (ms):{watch.ElapsedMilliseconds} {first.Status} {second.Status}";
            }
            catch (OperationCanceledException ocs)
            {
                duration.Text = ocs.Message;
            }

            /*这里不能用WaitAll,WaitAll方法会等任务完成的,这样就没法执行取消操作了
             * 只有在标记为 async 的方法中才能使用await操作符,作用是释放当前的线程,
             * 等待一个任务在后台完成。任务完成后,控制会回到方法中,从下一个语句继续。
             */

            Stream pixelStreem = graphBitmap.PixelBuffer.AsStream();
            pixelStreem.Seek(0, SeekOrigin.Begin);
            pixelStreem.Write(data, 0, data.Length);
            graphBitmap.Invalidate();
            graphImage.Source = graphBitmap;


        }
        private async Task gen(byte[] data,int partitionStart,int partitionEnd,CancellationToken token)
        {
            Task task = Task.Run(() => generateGraphData(data, partitionStart, partitionEnd, token));
            await task;
        }
        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;
                    //token.ThrowIfCancellationRequested();
                    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;
        }
    }
}

24.1.3 定义返回值的异步方法

可以用泛型Task类,类型参数TResult指定结果类型。如:

Task<int> calculateValueTask=Task.Run(()=>calculatValue(...));
...;
int calculatedData=calculateValueTask.Result;
...;
private int calculateValue(...){
    int someValue;
    ...;
    return someValue;
}

异步方法:

private async Task<int> calculateValueAsync(...){
    Task<int> generateResultTask=Task.Run(()=>calculateValue(...));
    await generateResultTask;
    return generateResultTask.Result;
}

上述代码中的方法看起来有点怪,因为返回类型是Task,但return 语句返回的却是int,实际上在定义async方法时编译器会对代码进行重构。所以是没问题的。调用有返回值的异步方法需要await:

int result =await calculateValueAsync(...);

24.1.4 异步方法注意事项

  • await操作符说明方法应该由一个单独的任务运行,调用代码暂停直至调用完成。调用代码使用的线程被释放供重用。这对于UI线程是非常重要的,它可以使UI保持灵敏响应
  • 有返回值的异步方法使用时一定要小心,一旦使用不当可能会造成死锁,使应用程序挂起。

例如:

private async void myMethod(){
    var data=generateResult();
    ...;
    message.Text=$"result: {data.Result}";
}
private async Task<string>generateResult(){
    string result;
    result=...;
    return result;
}

上述代码,在访问data.Result属性时才会运行generateResult方法的任务。data是任务引用,如果由于任务尚未运行造成Result属性不可用,访问该属性会阻塞当前线程,直到generateResult方法完成。而用于运行该方法的任务会在方法完成时尝试恢复当初调用它的线程,但该线程已阻塞了。解决方案是如下:

private async void myMethond(){
    var data=generateResult();
    ...;
    message.Text=$"result: {await data}";
}

24.1.5 异步方法和Windows Runtime API

如:

MessageDialog dlg=new MessageDialog("Message to user");
await dig.ShowAsync();

MessageDialog对象显示消息并等待用户按Close按钮。对话框在显示期间可能会阻塞应用程序。如果要同步显示对话框可以不添加await。

构建Windows应用程序一定要充分利用异步.

24.2 用PLINQ进行并行数据访问

24.2.1 用PLINQ增强遍历集合时的性能

如:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace c_24_2_1
{
    class Program
    {
        static void dowork()
        {
            Random rand = new Random();
            List<int> numbers = new List<int>();
            for(int i = 0; i < 10000; ++i)
            {
                int num = rand.Next(0, 200);
                numbers.Add(num);
            }
            var over100=from n in numbers.AsParallel()
                        where TestIfTrue(n>100)
                        select n;
            List<int> numOver100 = new List<int>(over100);
            Console.WriteLine($"There are {numOver100.Count} numbers over 100");
        }

        private static bool TestIfTrue(bool v)
        {
            Thread.SpinWait(1000);
            return v;
        }

        static void Main(string[] args)
        {
            dowork();
        }
    }
}

运行结果:

There are 4877 numbers over 100

C:\Users\xhh\Source\Repos\c_24_2_1\c_24_2_1\bin\Debug\netcoreapp3.1\c_24_2_1.exe (进程 28128)已退出,代码为 0。
按任意键关闭此窗口. . .

24.2.2 取消PLINQ查询

PLINQ查询时可以取消的。

CancellationToken tok=...;
...;
var orderInfoQuery=
    from c in CustomersInMemory.Cuscomers.AsParallel().WithCancellation(tok)
    join o in OrdersInfoMemory.Orders.AsParallel()
    om...;

24.3 同步对数据的并发访问

24.3.1 锁定数据

用关键字lock来提供锁定语义:

object myLockObjecyt=new object();
...;
lock(myLockObject){
    ...;//需要对共享资源进行独占访问的代码
}

24.3.2 用于协调任务的同步基元

  • ManualResetEventSlim类
  • SemaphoneSlim类
  • CountdownEvent类
  • ReaderWriterLockSlim类
  • Barrier类

24.3.3 取消同步

CancellationTokenSource canc=new CancellationTokenSource();
CancellationToken cancellationToken=canc.Token;
...;
SemaphoreSlim sem=new SemaphoreSlim(3);
...;
try{
    sem.Wait(cancellationToken)
}catch(OperationCanceledException e){
    ...;
}

24.3.4 并发集合类

  • ConcurrentBag
  • ConcurrentDictionary
  • ConcurrentQueue
  • CurrentStack

24.3.5 使用并发集合和锁实现线程安全的数据访问

下面我们实现一个基于数学计算和统计采样的计算 π \pi π的算法。

对于一个圆和它的外切正方形,若圆的面积为C,半径为r,正方形的面积为S,可以得出下面的关系:
C = π r 2 S = 4 r 2 = > r 2 = C π = S 4 = > π = 4 C S C=\pi r^2\\S=4r^2\\=>r^2=\frac{C}{\pi}=\frac{S}{4}\\=>\pi=\frac{4C}{S} C=πr2S=4r2=>r2=πC=4S=>π=S4C
求C/S的比值可以用统计学采样。可以生成一组随机点均匀分布在正方形中,统计有多少点落在圆中,这样就可以得出比值了。随机样本一定要足够多。

我们先用单线程来计算PI:

Program.cs

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;

namespace C_24_3_5
{
    class Program
    {
        private const int NUMPOINTS= 50000;
        private const int RADIUS =5000;
        static void Main(string[] args)
        {
            double PI = SerialPI();
            Console.WriteLine($"Geometric approximation of PI calculated serially: {PI}");
            Console.WriteLine();
        }

        private static double SerialPI()
        {
            List<double>pointsList=new List<double>();
            Random rand = new Random();
            int numPointsInCircle = 0;
            Stopwatch timer = new Stopwatch();
            timer.Start();
            try
            {
                for(int points = 0; points < NUMPOINTS; points++)
                {
                    int xCoord = rand.Next(RADIUS);
                    int yCoord = rand.Next(RADIUS);
                    double distanceFromOrigin = Math.Sqrt(xCoord * xCoord + yCoord * yCoord);
                    pointsList.Add(distanceFromOrigin);
                    doAdditionalProcessing();
                }
                foreach(double dutum in pointsList)
                {
                    if (dutum <= RADIUS)
                    {
                        numPointsInCircle++;
                    }
                }
                double pi = 4.0 * numPointsInCircle / NUMPOINTS;
                return pi;
            }
            finally
            {
                long milliseconds = timer.ElapsedMilliseconds;
                Console.WriteLine($"SerialPI Complete: Duration: {milliseconds} ms");
                Console.WriteLine($"Points in pointsList: {pointsList.Count},Points within circle: {numPointsInCircle}");
            }
        }

        private static void doAdditionalProcessing()
        {
            Thread.SpinWait(1000);
        }
    }
}

运行结果:

SerialPI Complete: Duration: 1941 ms
Points in pointsList: 50000,Points within circle: 39267
Geometric approximation of PI calculated serially: 3.14136


C:\Users\xhh\Source\Repos\C_24_3_5\C_24_3_5\bin\Debug\netcoreapp3.1\C_24_3_5.exe (进程 28136)已退出,代码为 0。
按任意键关闭此窗口. . .

添加一个并行版本的方法来计算PI:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
namespace C_24_3_5
{
    class Program
    {
        private const int NUMPOINTS= 50000;
        private const int RADIUS =5000;
        static void Main(string[] args)
        {
            double PI = SerialPI();
            Console.WriteLine($"Geometric approximation of PI calculated serially: {PI}");
            Console.WriteLine();

            PI = ParallelPI();
            Console.WriteLine($"Geometric approximation of PI calculated parallelly: {PI}");
            Console.WriteLine();
        }

        private static double ParallelPI()
        {
            List<double> pointsList = new List<double>();
            Random rand = new Random();
            int numPointsInCircle = 0;
            Stopwatch timer = new Stopwatch();
            timer.Start();
            try
            {
                Parallel.For(0, NUMPOINTS, (x) =>
                {
                    int xCoord = rand.Next(RADIUS);
                    int yCoord = rand.Next(RADIUS);
                    double distanceFromOrigin = Math.Sqrt(xCoord * xCoord + yCoord * yCoord);
                    pointsList.Add(distanceFromOrigin);
                    doAdditionalProcessing();
                });
                foreach (double dutum in pointsList)
                {
                    if (dutum <= RADIUS)
                    {
                        numPointsInCircle++;
                    }
                }
                double pi = 4.0 * numPointsInCircle / NUMPOINTS;
                return pi;
            }
            finally
            {
                long milliseconds = timer.ElapsedMilliseconds;
                Console.WriteLine($"ParallelPI Complete: Duration: {milliseconds} ms");
                Console.WriteLine($"Points in pointsList: {pointsList.Count},Points within circle: {numPointsInCircle}");
            }
        }

        private static double SerialPI()
        {
            List<double>pointsList=new List<double>();
            Random rand = new Random();
            int numPointsInCircle = 0;
            Stopwatch timer = new Stopwatch();
            timer.Start();
            try
            {
                for (int points = 0; points < NUMPOINTS; points++)
                {
                    int xCoord = rand.Next(RADIUS);
                    int yCoord = rand.Next(RADIUS);
                    double distanceFromOrigin = Math.Sqrt(xCoord * xCoord + yCoord * yCoord);
                    pointsList.Add(distanceFromOrigin);
                    doAdditionalProcessing();
                }
                foreach (double dutum in pointsList)
                {
                    if (dutum <= RADIUS)
                    {
                        numPointsInCircle++;
                    }
                }
                double pi = 4.0 * numPointsInCircle / NUMPOINTS;
                return pi;
            }
            finally
            {
                long milliseconds = timer.ElapsedMilliseconds;
                Console.WriteLine($"SerialPI Complete: Duration: {milliseconds} ms");
                Console.WriteLine($"Points in pointsList: {pointsList.Count},Points within circle: {numPointsInCircle}");
            }
        }

        private static void doAdditionalProcessing()
        {
            Thread.SpinWait(1000);
        }
    }
}

运行结果:

SerialPI Complete: Duration: 1942 ms
Points in pointsList: 50000,Points within circle: 39478
Geometric approximation of PI calculated serially: 3.15824

ParallelPI Complete: Duration: 434 ms
Points in pointsList: 24016,Points within circle: 20546
Geometric approximation of PI calculated parallelly: 1.64368


C:\Users\xhh\Source\Repos\C_24_3_5\C_24_3_5\bin\Debug\netcoreapp3.1\C_24_3_5.exe (进程 9104)已退出,代码为 0。
按任意键关闭此窗口. . .

注意并行版本的结果是有问题的,落在圆中的点数要少了很多。

我们进行一些修改:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
namespace C_24_3_5
{
    class Program
    {
        private const int NUMPOINTS= 50000;
        private const int RADIUS =5000;
        static void Main(string[] args)
        {
            double PI = SerialPI();
            Console.WriteLine($"Geometric approximation of PI calculated serially: {PI}");
            Console.WriteLine();

            PI = ParallelPI();
            Console.WriteLine($"Geometric approximation of PI calculated parallelly: {PI}");
            Console.WriteLine();
        }

        private static double ParallelPI()
        {
            ConcurrentBag<double> pointsList = new ConcurrentBag<double>();
            Random rand = new Random();
            int numPointsInCircle = 0;
            Stopwatch timer = new Stopwatch();
            timer.Start();
            try
            {
                Parallel.For(0, NUMPOINTS, (x) =>
                {
                    int xCoord, yCoord;
                    lock (pointsList)
                    {
                        xCoord = rand.Next(RADIUS);
                        yCoord = rand.Next(RADIUS);
                    }
                    double distanceFromOrigin = Math.Sqrt(xCoord * xCoord + yCoord * yCoord);
                    pointsList.Add(distanceFromOrigin);
                    doAdditionalProcessing();
                });
                foreach (double dutum in pointsList)
                {
                    if (dutum <= RADIUS)
                    {
                        numPointsInCircle++;
                    }
                }
                double pi = 4.0 * numPointsInCircle / NUMPOINTS;
                return pi;
            }
            finally
            {
                long milliseconds = timer.ElapsedMilliseconds;
                Console.WriteLine($"ParallelPI Complete: Duration: {milliseconds} ms");
                Console.WriteLine($"Points in pointsList: {pointsList.Count},Points within circle: {numPointsInCircle}");
            }
        }

        private static double SerialPI()
        {
            List<double>pointsList=new List<double>();
            Random rand = new Random();
            int numPointsInCircle = 0;
            Stopwatch timer = new Stopwatch();
            timer.Start();
            try
            {
                for (int points = 0; points < NUMPOINTS; points++)
                {
                    int xCoord = rand.Next(RADIUS);
                    int yCoord = rand.Next(RADIUS);
                    double distanceFromOrigin = Math.Sqrt(xCoord * xCoord + yCoord * yCoord);
                    pointsList.Add(distanceFromOrigin);
                    doAdditionalProcessing();
                }
                foreach (double dutum in pointsList)
                {
                    if (dutum <= RADIUS)
                    {
                        numPointsInCircle++;
                    }
                }
                double pi = 4.0 * numPointsInCircle / NUMPOINTS;
                return pi;
            }
            finally
            {
                long milliseconds = timer.ElapsedMilliseconds;
                Console.WriteLine($"SerialPI Complete: Duration: {milliseconds} ms");
                Console.WriteLine($"Points in pointsList: {pointsList.Count},Points within circle: {numPointsInCircle}");
            }
        }

        private static void doAdditionalProcessing()
        {
            Thread.SpinWait(1000);
        }
    }
}

运行结果:

SerialPI Complete: Duration: 1947 ms
Points in pointsList: 50000,Points within circle: 39216
Geometric approximation of PI calculated serially: 3.13728

ParallelPI Complete: Duration: 204 ms
Points in pointsList: 50000,Points within circle: 39232
Geometric approximation of PI calculated parallelly: 3.13856


C:\Users\xhh\Source\Repos\C_24_3_5\C_24_3_5\bin\Debug\netcoreapp3.1\C_24_3_5.exe (进程 28404)已退出,代码为 0。
按任意键关闭此窗口. . .

这个结果就是没问题的了。

你可能感兴趣的:(Visual,C#)