你的位置:首页 > ASP.net教程

[ASP.net教程]异步方法的意义何在,Async和await以及Task的爱恨情仇,还有多线程那一家子。


  前两天刚感受了下泛型接口的in和out,昨天就开始感受神奇的异步方法Async/await,当然顺路也看了眼多线程那几个。其实多线程异步相关的类单个用法和理解都不算困难,但是异步方法Async/await这东西和Task搅到了一起就有点花花肠子。要单说用法其实也好理解,也有不少文章写了。看过上一篇的同学知道,不弄清楚来龙去脉,这世界总感觉不够高清。异步方法究竟怎么个异步法,为什这样设计,有什么意义?昨天想到今天,感觉终于算是讲得通了,一点愚见记下来分享给大家。

  先不着急直奔主题,看看多线程那一家子,再看他们和Async怎么搞基的。

  1 线程和线程池Thread&ThreadPool

  最基本的线程调用工具

      //线程      //线程初始化时执行方法可以带一个object参数,为了传入自定义参数,所以执行需单独调用用于传参。      Console.WriteLine("执行线程");      Thread th = new Thread((objParam) =>       {        Console.WriteLine("线程启动,执行匿名方法,有无参数{0}", objParam != null);      });      th.IsBackground = true;      object objP = new object();      th.Start(objP);      //线程池      //线程池初始化执行方法必须带一个object参数,接受到的值是系统默认NULL(不明),所以初始化完成自动调用      Console.WriteLine("执行线程池");      ThreadPool.QueueUserWorkItem((objparam) =>      {        Console.WriteLine("线程池加入的匿名方法被执行。");      });

  执行结果:

  可以看到这Thread和ThreadPool执行不影响主进程执行。Thread和ThreadPool接受的都是委托类型,所以可以单独定义方法在初始化的时候传入,接受的委托都返回void,所以都不能在线程里有返回值。Thead是单开线程,ThreadPool是使用系统的线程池所以性能更好。参数相关注释里有写,其他特性我们不深究,我们知道这两的原理就是使用线程执行无返回值的方法的即可。

  2 并行循环Parallel

  是用多个线程执行循环的工具

      int result = 0;      int lockResult = 0;      object lb = new object();      //并行循环      //并行应该用于一次执行多个相同任务,或计算结果和循环的游标没有关系只和执行次数有关系的计算      Console.WriteLine("执行并行循环");      Parallel.For(0, 10, (i) =>      {        result = result + 2;        //lock只能lock引用类型,利用引用对象的地址唯一作为锁,实现lock中的代码一次只能一个线程访问        //lock让lock里的代码在并行时变为串行,尽量不要在parallel中用lock(lock内的操作耗时小,lock外操作耗时大时,并行还是起作用)        lock(lb)        {          lockResult = lockResult + 2;          Thread.Sleep(100);          Console.WriteLine("i={0},lockResult={1}", i, lockResult);        }        Console.WriteLine("i={0},result={1}", i, result);      });

  说明一下,为了验证并行循环的执行过程,加入lock玩了一下。lock为什么只能lock引用对象是我推测的,如有偏差,概不负责。

  执行结果:

  Parallel用法很简单,就是Parallel.For(游标开始值, 游标结束, int参数的Action),传入action的方法接受的int参数就是当前执行的游标。

  跑题开始-------------------------------(手贱要在并行里写lock还要sleep刚好形成规律,以下是写博时发现的,没兴趣的同学可以跳过)

  通过结果我们可以看出,首先执行顺序是随机的,可以猜到一次是把游标的取值分别当参数传给多个线程执行action即可。后面的结果也验证了这一点,lockResult不用说,不管多少线程到这都得排队执行,所以结果递增。再看result,上来就变成了10,可以推出遇到lock之前已经被加了5次,那么应该是一次4个线程喽(大家肯定觉得应该是5个,开始我也是这样觉得,往下看)。

  再看result其实也不是没规律,可以看出从10到20也是递增,但到了20就不增加了(因缺思亭)。我们模拟下(按5个线程模拟不符合结果,我就直接按合理的情况推一遍)。

  1 首先可以4个线程ABCD同时执行,都到了lock这停住,那这时result被加了4次是8。

  2 然后一个线程A执行lock里的代码,其他的BCD等待(不是sleep仍然占用cpu),执行完输出lockResult=2(第一行)。

  这时继续往下应该输出result=8对吧,为什么是result=10。注意lock里有一个Thread.Sleep(100),这就是关键。在lock里sleep会怎样,当前线程A释放cpu 100ms,这时就可以再来一个线程E执行到lock这也停住了,result是不是就是10了。

  3 这个线程A醒来优先级最高挤掉一个线程往下继续输出result=10(第二行)。这时刚才被挤掉的线程又恢复占用cpu状态,就是BCDE四个线程。

  4 同理,BCDE四个等待线程的又有一个进入lock然后又sleep,又可以有一个线程来把result加2,这时循环这个过程,result也呈现出规律。

  5 为什么result后面几次都是20,因为总共执行10次,首先四个线程执行了4次,然后一个新线程执行第5次后,第1次执行的线程才输出第5次执行后的结果,第2次输出第6次。。。第6次输出第10次(第6行就是result=20),后面四次已经执行过result加2,所以只输出结果20。

  如果把Thread.Sleep(100)去掉result就不再有这么明显的规律。因为sleep让cpu可以释放与lock等待共同作用让线程执行形成一个先后顺序的队列。sleep放到lock外也不行,sleep会释放cpu,放到lock外,没有lock占用cpu,lock前就不一定执行了几次。

  为什么一次是四个线程呢,很容易想到,我CPU四核的。。。就这么简单。。

  跑题结束---------------------------------

  通过以上分析,并行是个什么东西大家应该有所了解了,继续。

  3 任务Task

  一个可以有返回值(需要等待)的多线程工具

      //任务      Task.Run(() =>      {        Thread.Sleep(200);        Console.WriteLine("Task启动执行匿名方法");      });      Console.WriteLine("Task默认不阻塞");      //获取Task.Result会造成阻塞等待task执行      int r = Task.Run(() =>      {        Console.WriteLine("Task启动执行匿名方法并返回值");        Thread.Sleep(1000);        return 5;      }).Result;      Console.WriteLine("返回值是{0}", r);

  执行结果:

  用法如上,好像用的线程池。传入方法不能有参数,可以有返回值。要获得结果,要在Run()(返回Task<T>类型)之后调用Task<T>类型的Result属性获取。可以看出,获取结果时,Task是会阻塞当前进程的,等待线程执行完毕才继续。

  Task好用,关键点就是有返回值,可以获取结果。

------------------------------------------------关于多线程就扯这么多,终于进入主题Async异步方法--------------------------------------------------------------

  异步方法Async&await&Task

  一些点:

  1 异步方法需要Async关键字修饰

  2 异步方法的返回类型只能是void或Task<T>

  3 只有异步方法内使用了 await关键词描述的 有返回值的 线程Task才有效

  4 await只能用在Async方法里的Task上,await会让当前方法等待Task执行完毕再执行

  5 返回值类型是T时,方法返回类型就是Task<T>

  既然异步方法一定要用Task才有效,就写了一个用了Task的普通方法和一个异步方法测试,如下:

    //【意义】异步方法的意义就是保证一个进程使用多线程多次执行一个方法时,不会因为其中某一次执行阻塞调用进程    //【原理】利用方法内Task调用新线程,await使方法内等待Task结果时调用进程不被阻塞,多次调用相当于多个线程并行。    //【区别】普通方法只用Task,当方法内需要Task返回值时,等待Task结果就会阻塞调用进程。    //【应用】主要应用在没有返回值,操作耗时长(用Task)且需要Task返回结果的方法。(不需要Task返回值的话和普通方法调用Task性能一样,因为普通方法也没有阻塞进程的风险了)    //   (有返回值调用时await获取结果就重蹈阻塞进程的覆辙了)    public async Task<int> MethodA(DateTime bgtime, int i)    {      int r = await Task.Run(() =>      {        Console.WriteLine("异步方法{0}Task被执行", i);        return i * 2;      });      Thread.Sleep(100);      Console.WriteLine("异步方法{0}执行完毕,结果{1}", i, r);      if (i == 49)      {        Console.WriteLine("用时{0}", (DateTime.Now - bgtime).TotalMilliseconds);      }      return r;    }    //和异步方法调用对比测试    public int MethodC(DateTime bgtime, int i)    {      int r = Task.Run(() =>      {        Console.WriteLine("普通多线程方法{0}Task被执行", i);        return i * 2;      }).Result;      Thread.Sleep(100);      Console.WriteLine("普通方法{0}执行完毕,结果{1}", i, r);      if (i == 49)      {        Console.WriteLine("用时{0}", (DateTime.Now - bgtime).TotalMilliseconds);      }      return r;    }

  测试结果:

  

  可以发现普通方法由于阻塞执行都是按顺序执行,多线程失去意义。异步方法则并行执行,重要的是计算结果一样。

  重要的结论都写在注释里了。相信透过代码和结果大家或多或少能理解了吧,以及上面提到的几个点为什么这样设计透过结论也能推出一二。我就不一一就结论进行分析和验证了,相信我为了得到这些结论做的验证不会少。

  还有一点,为什么返回值类型是T,方法返回类型需要是Task<T>,我的推测是这样的:要达到异步方法内等待线程结果不阻塞调用进程,这个方法本身就应该在线程中执行。所以不管返回值类型是什么,都被替换成类型Task<T>。这样被调用时相当于一个Task.Run(),也就可以实现异步方法await了(虽然这样就失去异步的意义但有原因)。比如自己在一个普通方法里写异步方法调用AsyncMethod(),系统会给你提示说不加await程序会继续执行,建议加await等待其结果,你要是加了后就报错了,说await只能用在async方法里,是不是有点蹊跷。只能用在async方法里的话,那就是说只有异步方法内才能await AsyncMethod()(等待异步方法调用的结果)。所以最终调用异步方法的肯定是一个普通方法,就不能await了,也就实现了异步方法并行,结果就是:异步方法内可以等待或者不等待另一个异步方法的调用,而最上层的异步方法是由普通方法调用不能等待所以并行,且最上层的异步方法的返回值没有意义。

  最后一定要区别异步方法和普通多线程方法,他们的关键区别就是是否可以单独等待线程的执行结果。不要把异步方法当多线程方法用了。

  觉得有帮助的同学可以推荐或者顶一下,这两天研究这个都要精尽人亡了,我睡了。