• C#多线程和异步(三)——一些异步编程模式



    一、任务并行库

      任务并行库(Task Parallel Library)是BCL中的一个类库,极大地简化了并行编程,Parallel常用的方法有For/ForEach/Invoke三个静态方法。在C#中for/foreach循环使用十分普遍,如果迭代不依赖与上次迭代的结果时,把迭代放在 不同的处理器上并行处理 将很大地提高运行效率,Parallel.For和Parallel.ForEach就是为这个目的而设计的。

      看一个Parallel.For/ForEach的栗子:

    复制代码
              static void Main(string[] args)
             {
                 //Parallel.For  计算0到6的平方
                 Parallel.For(1, 6, i =>
                 {
                     Console.WriteLine($"{i}的平方是{i*i}");
                 });
     
                 //Parallel.ForEach 计算每个字符串的长度
                string[] strs = { "We", "hold", "these", "truths" };
                Parallel.ForEach(strs, i => Console.WriteLine($"{i}有{i.Length}个字节"));
                Console.ReadKey();
            }
    复制代码

      运行结果:

      如果我们想并行执行多个任务,可以使用 Parallel.Invoke(Action[] actions) 方法,看一个栗子:

    复制代码
            static void Main(string[] args)
            {
                Parallel.Invoke(
                    () => { Console.WriteLine($"并行执行任务1,线程Id为{Thread.CurrentThread.ManagedThreadId}"); },
                    () => { Console.WriteLine($"并行执行任务2,线程Id为{Thread.CurrentThread.ManagedThreadId}"); }
                    );
                Console.ReadKey();
            }
    复制代码

    执行结果如下:

    二、计时器(Timer)

      计时器提供了一种 定期重复运行异步方法 的方式,当计时器到期后,系统从线程池中的线程上开启一个回调方法,把state作为参数,并开始运行。

    Timer最常用的构造函数如下:

    Timer(TimeCallback callback,object state,uint dueTime, uint period)

    callback是一个返回值为void的委托,state为传入callback的参数,dueTime为第一次调用前的时间,period为两次调用的时间间隔

     一个栗子:

    复制代码
     1  class Program
     2     {
     3         int count = 0;
     4         void Run(object state)
     5         {
     6             Console.WriteLine("{0},已经调用了{1}次了", state, ++count);
     7         }
     8         static void Main(string[] args)
     9         {
    10             Program p = new Program();
    11             //2000毫秒后开始调用,每次间隔1000毫秒
    12             Timer timer = new Timer(p.Run, "hello", 2000, 1000);
    13             Console.WriteLine("Timer start");
    14             
    15             Console.ReadLine();
    16         }
    17     }
    复制代码

    执行结果:

    三、委托执行异步

      委托执行异步是早期执行异步的一种方式,特别是早几年进行网络编程时用的比较多。现在我们完全可以使用更优秀的其他异步编程模式去替代它。有时候我们会查看早期的代码,我们在这里简单介绍下委托执行异步的方法。使用委托执行异步,使用的是引用方法,如果一个委托对象在调用列表中只有一个方法(这个方法就是引用方法),它就可以异步执行这个方法。委托类有两个方法 BeginIvoke和EndInvoke 。

       BeginInvoke :执行BeginInvoke方法时,会线程池中获取一个独立线程来执行引用方法,并立即返回一个实现IAsyncResult接口的对象的(该对象包含了线程池中线程运行异步方法的状态),调用线程不阻塞,而引用方法在线程池的线程中并行执行。

       EndInvoke  : 获取异步方法调用返回的值,并释放资源,该方法把异步方法的返回值作为自己的返回值。

    委托执行异步编程的3种模式:

      等待一直到完成(wait-until-done):在发起了异步方法,原始线程执行到EndInvoke时就中断并且等异步方法完成完成后再继续。

      轮询(polling):原始线程定期检查发起的线程是否完成(通过IAsyncResult.IsCompleted属性判断),如果没有则继续进行原始线程中的任务。

      回调(callback):原始线程一直执行,无需等待或检查发起的线程是否完成,在发起的线程中的引用方法完成之后,发起线程会调用回调方法,由回调方法在调用EndInvoke之前处理异步方法的结果。

    3.1 等待一直到完成模式

      原始线程执行到EndInvoke,如果异步任务没有完成就一直等待

    复制代码
     1     delegate int MyDel(int first,int second);//委托声明
     2     class Program
     3     {
     4         static int Sum(int x, int y)
     5         {
     6             Thread.Sleep(1000);
     7             return x + y;
     8         }
     9         static void Main(string[] args)
    10         {
    11             MyDel del = Sum;
    12             //调用异步操作(第三个参数是回调函数,第四个参数是额外的值)
    13             IAsyncResult iar = del.BeginInvoke(3, 5, null, null);
    14             
    15             //doSomehing...
    16             
    17             //执行EndInvoke,如果引用方法Sum没有执行完成,主线程就等待其完成
    18             int result = del.EndInvoke(iar);
    19             Console.WriteLine(result);
    20         }
    21     }
    复制代码

    3.2 轮询模式

      定期查询任务是否完成:

    复制代码
     1     delegate int MyDel(int first,int second);//委托声明
     2     class Program
     3     {
     4         static int Sum(int x, int y)
     5         {
     6             Thread.Sleep(1000);
     7             return x + y;
     8         }
     9         static void Main(string[] args)
    10         {
    11             MyDel del = Sum;
    12             IAsyncResult iar = del.BeginInvoke(3, 5, null, null);
    13             
    14             //通过iar.IsCompleted定期查询完成状态
    15             while (!iar.IsCompleted)//IsCompleted表示调用的异步操作是否完成
    16             {
    17                 //doSomething
    18                 Thread.Sleep(300);
    19                 Console.WriteLine("no done");
    20             }
    21             int result = del.EndInvoke(iar);
    22             Console.WriteLine(result);
    23             Console.ReadKey();
    24         }
    25     }
    复制代码

    3.3 回调模式

    原始线程执行委托的BeginInvoke后就不管新线程的事了,委托中的引用方法执行完成后,在回调函数中获取结果并处理,执行委托的EndInvoke方法

    复制代码
     1     delegate int MyDel(int first,int second);//委托声明
     2     class Program
     3     {
     4         static int Sum(int x, int y)
     5         {
     6             Thread.Sleep(1000);
     7             return x + y;
     8         }
     9         
    10         //回调方法的签名和返回值类型必须和AsyncCallBack委托类型一致
    11         //输入参数为IAsyncResult,返回值是Void类型
    12        static void CallWhenDone(IAsyncResult iar){
    13            AsyncResult ar = (AsyncResult)iar;
    14            MyDel del = (MyDel)ar.AsyncDelegate;
    15            int result = del.EndInvoke(iar);
    16            Console.WriteLine("回调函数执行EndInvoke");
    17            Console.WriteLine("result:{0}", result);
    18            Console.WriteLine("回调函数完成");
    19         }     
    20         
    21         static void Main(string[] args)
    22         {
    23             MyDel del = Sum;
    24             //执行BeginInvoke方法后原始线程就不用管了,在自定义的回调函数(CallWhenDone)中执行EndInvoke方法
    25             IAsyncResult iar = del.BeginInvoke(3, 5, CallWhenDone, null);
    26             Console.WriteLine("开启新线程,异步任务完成后执行回调函数");
    27             //doSomething
    28             Console.WriteLine("回调执行不阻塞原始线程");
    29             Console.ReadKey();
    30         }
    31     }
    复制代码

    执行结果:

    还有一些其他的异步编程模式如BackgroundWorker等,这里不再过多介绍。

     一点补充(Windbg)

    1 cpu占用过高

      我们使用多线程时有时会遇到cpu占用过高、内存爆满的情况,快速定位异常线程是多线程开发中必须熟悉的技能。cpu占用过高一般是由死循环造成的,看下边一个简单的栗子,Run方法内部有死循环,程序运行后会 占用大量的cpu资源:

    复制代码
    namespace MyApp
    {
        class Program
        {
            static void Main(string[] args)
            {
                Run();
                Run2();
                Console.ReadKey();
            }
      //死循环,会造成cpu内存占用过高
            static void Run()
            {
                Thread th = new Thread(() =>
                {
                    while (true)
                    {
                        Console.WriteLine("hello windbg");
                    }
                });
                th.Start();
            }
      //不会占用太高的cpu资源
            static void Run2()
            {
                Thread th = new Thread(() =>
                {
                    while (true)
                    {
                        Thread.Sleep(1000);
                        Console.WriteLine("hello windbg2");
                    }
                });
                th.Start();
            }
        }
    }
    复制代码

      程序运行后cpu资源占用过高,怎么去定位呢?这里采用Windbg简单演示cpu占用过高的异常定位,下载地址:Windbg下载。安装完成后,界面如下所示:

    1.生成Dump文件

      这里MyApp生成为x64位的Release版本,点击MyApp.exe文件运行,打开【任务管理器】,找到MyApp,右键选择【创建转储文件】即可生成dump文件。

    2.Windbg分析dump文件

      打开Windbg,选择【文件】->【Open dump file】->找到上一步生成的dump文件即可。

       执行以下命令加载符号和sos库

    .sympath SRV*c:localsymbols*http://msdl.microsoft.com/download/symbols
    .reload
    .load C:WindowsMicrosoft.NETFramework64v4.0.30319SOS.DLL

      通过命令 !threads 查看线程:

       死循环会长期占有cpu,通过 !runaway 查看各个线程的运行时间:

       我们看到 4eac线程的运行时间最长,通过命令 ~~[4eac] ; !clrstack 查看线程堆栈信息:

      我们看到异常定位在MyApp的Program类的第24行,查看我们的代码,找到这个位置,发现这里是一个while(true)死循环,定位结束。

    2 内存爆满

      内存爆满也是异常遇到的问题,如大量拼接字符串会占用较大的内存,看下边的一个栗子,程序代码如下:

    复制代码
        class Program
        {
            static void Main(string[] args)
            {
                Console.WriteLine("开始执行..");
                GetBigString();
                Console.ReadKey();
            }
            //大字符串拼接
            static void GetBigString()
            {
                String str = "";
                for (int i = 0; i < 10000000; i++)
                {
                    str+=$"hello{i}";
                }
                Console.WriteLine(str);
            }
        }
    复制代码

         内存爆满最常见原因是大量创建某个类型的变量,问题定位方法和上边定位cpu占用高的定位差不多。首先生成dump文件,然后用Windbg打开,加载符号和sos库,然后执行 !dumpheap –stat 查看各个类型的数量和尺寸,我们看到string类型数量和占用的资源很多:

     

      通过 !DumpHeap /d -mt 00007ff8878c74c0 查看当前的方法表,如下:

      点开一个地址,具体内容如下:

      通过字符串内容是【hello0hello1...】和string类型数量多、尺寸大,我们再去在代码中查找很容易定位到问题代码。

    小结:Windbg可以查看clr级别内容,在开发中对我们优化代码和异常定位有不错的帮助,这里只是简单介绍Windbg的基本用法,有兴趣的小伙伴可以研究下官方教程

    【转载:原文链接:https://www.cnblogs.com/wyy1234/p/9178647.html

      

  • 相关阅读:
    ASP.NET页面生命周期总结(完结篇)
    ASP.NET页面生命周期总结(2)
    ASP.NET页面生命周期总结(1)
    springboot-简介
    python-day2
    python-day1
    jsoup解析页面
    httpclient模拟浏览器
    httpclient
    变量名和函数名重复的话
  • 原文地址:https://www.cnblogs.com/Mask71/p/12094873.html
Copyright © 2020-2023  润新知