zoukankan      html  css  js  c++  java
  • .Net中的异步编程总结

    一直以来很想梳理下我在开发过程中使用异步编程的心得和体会,但是由于我是APM异步编程模式的死忠,当TAP模式和TPL模式出现的时候我并未真正的去接纳这两种模式,所以导致我一直没有花太多心思去整理这两部分异步编程模型。今天在CodeProject上面偶然间看到一篇关于异步编程的文章,概括总结的非常好,省去了我自己写的麻烦,索性翻译过来,以飨各位。

          在阻塞和并行编程过程中,异步和多线程是非常重要的特性。异步编程可以涉及到多线程,也可以不用涉及。如果我们能够把二者放到一起来讲解的话,我们会理解的更快一些。所以今天在这里,我主要想讲解的内容是:

    1. 异步编程
    2. 是否需要多线程
    3. TAP编程模型
    4. 并行编程

    首先来说明异步编程

    所谓的异步编程就是指 当前的操作独立于主操作流程。在C#编程中,我们总是从进入Main方法开始 ,Main方法返回结束。在开始和结束之间的所有操作,都会挨个的执行,下一个操作必须等到上一个操作完毕才开始执行,我们看看如下代码:

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

    “DoTaskOne”方法必须在“DoTaskTwo”方法之前执行。也就是发生了阻塞现象。

    但是在异步编程中,方法的调用并不会阻塞主线程。当方法被调用后,主线程仍会继续执行其他的任务。这种情形,一般都是使用Thread或者是Task来实现的。

    在上面的场景中,如果我们让方法“DoTaskOne”异步执行,那么主线程将会立即返回,然后执行“DoTaskTwo”方法。

    在.NET中,我们可以利用Thread类或者异步模式来创建我们自己的线程来实现异步编程。在.NET中有三种不同的异步模型:

    1.APM模型

    2.EAP模型

    但是,遗憾的是,上面的这两种模型都不是微软所推荐的,所以我们将不对其做具体的讨论。如果你对这两种模型感兴趣,请移步:

    https://msdn.microsoft.com/en-us/library/ms228963(v=vs.110).aspx

    https://msdn.microsoft.com/en-us/library/ms228969(v=vs.110).aspx

    3.TAP模型:这是微软所推荐的,我们稍后将会详细的进行讲解。

    我们是否需要启用多线程

    如果我们在.NET 4.5版本中使用异步编程模型,绝大部分情况下我们无需手动创建线程。因为编译器已经为我们做了足够多的工作了。

    创建一个新线程不仅耗费资源而且花费时间,除非我们真的需要去控制一个多线程,否则是不需要去创建的。因为TAP模型和TPL模型已经为异步编程和并行编程提供了绝好的支持。TAP模型和TPL模型使用Task来进行操作,而Task类使用线程池中的线程来完成操作的。

    Task可以在如下场景中运行:

    1. 当前线程
    2. 新线程
    3. 线程池中的线程
    4. 没有线程

    在我们使用Task过程中,我们无需担心线程的创建或者是使用,因为.NET framework已经为我们处理好了各种细节。但是如果我们需要控制线程的话,比如说以下的场景:

    1. 为线程设置名称
    2. 为线程设置优先级
    3. 设置线程为后台线程

    那么我们不得不使用Thread类来控制。

    async和await关键字

    .NET framework引入了两个新的关键字“async”和“await”来执行异步编程。当在方法上使用await关键字的时候,我们需要同时使用async关键字。await关键字是在调用异步方法之前被使用的。它主要是使方法的后续执行编程异步的,例如:

    复制代码
    private asyncstatic void CallerWithAsync()// async modifier is used 
    {
      // await is used before a method call. It suspends 
      //execution of CallerWithAsync() method and control returs to the calling thread that can
      //perform other task.
      string result = await GetSomethingAsync();
    
      // this line would not be executed before  GetSomethingAsync() //method completes
      Console.WriteLine(result);
    }
    复制代码

    在这里,async关键字只能够被那些返回Task或者是void的方法所使用。它不能被用于Main入口方法上。

    我们不能够在所有的方法上使用await关键字,因为一旦方法上有了await关键字,那么我们的返回类型就变成了“awaitable “类型。下面的集中类型是” awaitable “:

    1. Task
    2. Task<T>

    TAP模型

    首先,我们需要一个能够返回Task或者Task<T>的异步方法。我们可以通过如下方式来创建Task:

    1. Task.Factory.StartNew方法:在.net 4.5版本之前(.net 4)中,这是默认的创建和组织task的方式。
    2. Task.Run或者Task.Run<T>方法:从.net 4.5版本开始,这个方法被引进来了。这个方法足以能够应付大多数的场景。
    3. Task.FromResult方法:如果方法已经执行完毕并返回结果,我们可以使用这个方法来创建一个task。

    Task.Factory.StartNew还有一些高级应用场景,请移步:http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx

    下面的链接则展示了创建Task的几种方式: 
    http://dotnetcodr.com/2014/01/01/5-ways-to-start-a-task-in-net-c/

    Task的创建和等待

    我们接下来将会利用Task.Run<T>方法来创建我们自己的Task。它会将某个特殊的方法放入位于ThreadPool的执行队列中,然后会为这些方法返回一个task句柄。下面的步骤将会为你展示我们如何为一个同步方法创建异步的Task的:

    1. 我们有一个同步的方法,需要耗费一些时间来执行完毕:
       static string Greeting(string name)
       {
            Thread.Sleep(3000);
            return string.Format("Hello, {0}", name);
        }
    1. 为了让此方法一步执行,我们需要对其进行异步方法的包装。这个异步方法我们暂定为“GreetingAsync“。
    复制代码
    static Task<string> GreetingAsync(string name)
    {
          return Task.Run<string>(() =>
                          {
                               return Greeting(name);
                           });
    }
     
    复制代码

    现在我们可以使用await关键字来调用GreetingAsync方法

    复制代码
    private asyncstatic void CallWithAsync()
     {
          //some other tasks
          string result = awaitGreetingAsync("Bulbul");
         //We can add  multiple ‘await’ in same ‘async’ method
         //string result1 = await GreetingAsync(“Ahmed”);
         //string result2 = await GreetingAsync(“Every Body”);
         Console.WriteLine(result);
     }
    复制代码

    当“CallWithAsync”方法被调用的时候,它首先像正常的同步方法被调用,直到遇到await关键字。当它遇到await关键字的时候,它会暂定方法的执行,然后等待“GreetingAsync(“Bulbul”)”方法执行完毕。在此期间,这个控制流程会返回到“CallWithAsync”方法上,然后调用者就可以做其他的任务了。

    当“GreetingAsync(“Bulbul”)”方法执行完毕以后,“CallWithAsync”方法就会唤醒在await关键字后面的其他的方法,所以在这里它会继续执行“Console.WriteLine(result)”方法。

    1. 任务的继续执行: “ContinueWith”方法则表明任务的持续执行。
    复制代码
    private static void CallWithContinuationTask()
       {
            Task<string> t1 = GreetingAsync("Bulbul");
            t1.ContinueWith(t =>
                              {
                                    string result = t.Result;
                                    Console.WriteLine(result);
                                 });
       }
    复制代码

    当我们使用ContinueWith方法的时候,我们无需使用await关键字。因为编译器会自动的将await关键字放到正确的位置上。

    等待多个异步方法

    让我们先看下面的代码:

    复制代码
      private asyncstatic void CallWithAsync()
          {
                string result = awaitGreetingAsync("Bulbul");
                string result1 = awaitGreetingAsync("Ahmed");
                Console.WriteLine(result);
                Console.WriteLine(result1);
       }
    复制代码

    在这里,我们在顺序的等待两个方法被执行。GreetingAsync("Ahmed")方法将会在GreetingAsync("Bulbul")执行后,再执行。但是,如果result和result1彼此不是独立的,那么await关键字这样用是不合适的。

    在上面的场景中,我们其实是无需使用await关键字的。所以方法可以被改成如下的样子:

    复制代码
     private async static void MultipleAsyncMethodsWithCombinators()
           {
     
                Task<string> t1 = GreetingAsync("Bulbul");
                Task<string> t2 = GreetingAsync("Ahmed");
                await Task.WhenAll(t1, t2);
                Console.WriteLine("Finished both methods.
     " +
                            "Result 1: {0}
     Result 2: {1}", t1.Result, t2.Result);
        }
    复制代码

    在这里我们用了Task.WhenAll方法,它主要是等待所有的Task都完成工作后再触发。Task类还有另一个方法:WhenAny,它主要是等待任何一个Task完成就会触发。

    异常处理

    当进行错误处理的时候,我们不得不将await关键字放到try代码块中。

    复制代码
    private asyncstatic void CallWithAsync()
    {
           try
           {
                string result = awaitGreetingAsync("Bulbul");
           }
           catch (Exception ex)
            {
              Console.WriteLine(&ldquo;handled {0}&rdquo;, ex.Message);
            }
    }
    复制代码

    但是,如果我们有多个await关键字存在于try代码快中,那么只有第一个错误被处理,因为第二个await是无法被执行的。如果我们想要所有的方法都被执行,甚至当其中一个抛出异常的时候,我们不能使用await关键字来调用他们,我们需要使用Task.WhenAll方法来等待所有的task执行。

    复制代码
    private asyncstatic void CallWithAsync()
                  {
                     try
                       {
     
                           Task<string> t1 = GreetingAsync("Bulbul");
     
                          Task<string> t2 = GreetingAsync("Ahmed");
     
                          await Task.WhenAll(t1, t2);
                      }
                   catch (Exception ex)
                   {
                   Console.WriteLine(&ldquo;handled {0}&rdquo;, ex.Message);
                 }
           }
    复制代码

    尽管所有的任务都会完成,但是我们可以从第一个task那里看到错误。虽然它不是第一个抛出错误的,但是它是列表中的第一个任务。

    如果想要从所有的任务中获取错误,那么有一个方式就是将其在try代码块外部进行声明。然后检查Task方法的“IsFaulted”属性。如果有错误抛出,那么其“IsFaulted”属性为true。

    示例如下:

     
    复制代码
     static async void ShowAggregatedException()
               {
                  Task taskResult = null;
                  try 
                 {
                      Task<string> t1 = GreetingAsync("Bulbul");
                      Task<string> t2 = GreetingAsync("Ahmed");
                      await (taskResult = Task.WhenAll(t1, t2));
               }
              catch (Exception ex)
              {
                 Console.WriteLine("handled {0}", ex.Message);
                 foreach (var innerEx in taskResult.Exception.InnerExceptions)
                {
                   Console.WriteLine("inner exception {0}", nnerEx.Message);
               }
            }
      }
     
    复制代码

    Task的取消执行

    如果直接使用ThreadPool中的Thread,我们是无法进行取消操作的。但是现在Task类提供了一种基于CancellationTokenSource类的方式来取消任务的执行,可以按照如下步骤来进行:

    1. 异步方法需要附带一个“CancellationToken”类型。
    2. 创建CancellationTokenSource类的实例:
    3. 将CancellationToken传递给异步方法:
    4. 对于长时间执行的方法,如果想取消的话,我们需要调用CancellationToken的ThrowIfCancellationRequested方法。
    5. 捕捉Task抛出的 OperationCanceledException。
    6. 现在,如果我们通过调用CancellationTokenSource的cancel方法来取消当前的操作的话,对于那些长时间运行的操作,将会抛出OperationCanceledException错误。我们也可以通过设置超时时间来取消任务。更多关于CancellationTokenSource类的信息,请移步:https://msdn.microsoft.com/en-us/library/system.threading.cancellationtokensource%28v=vs.110%29.aspx
    复制代码
    var cts = new CancellationTokenSource();
         Task<string> t1 = GreetingAsync("Bulbul", cts.Token);
    static string Greeting(string name, CancellationToken token)
    {
        Thread.Sleep(3000);
        token.ThrowIfCancellationRequested();
        return string.Format("Hello, {0}", name);
    }
    复制代码

    下面让我们看看如何设置超时时间来取消任务的执行:

    复制代码
    static void Main(string[] args)
            {
                CallWithAsync();
                Console.ReadKey();           
            }
     
            async static void CallWithAsync()
            {
                try
                {
                    CancellationTokenSource source = new CancellationTokenSource();
                    source.CancelAfter(TimeSpan.FromSeconds(1));
                    var t1 = await GreetingAsync("Bulbul", source.Token);
                }
                catch (OperationCanceledException ex)
                {
                    Console.WriteLine(ex.Message);
                }
            }
     
            static Task<string> GreetingAsync(string name, CancellationToken token)
            {
                return Task.Run<string>(() =>
                {
                    return Greeting(name, token);
                });
            }
     
            static string Greeting(string name, CancellationToken token)
            {
                Thread.Sleep(3000);
                token.ThrowIfCancellationRequested();
                return string.Format("Hello, {0}", name);
            }
     
    复制代码

    并行编程

    在.net 4.5中,存在一个叫做“Parallel”的类,这个类可以进行并行操作。当然这种并行和那些充分利用cpu计算能力的Thread 是有差别的,简单说起来,它有两种表现方式:

    1.数据并行。 如果我们有很多的数据需要计算,我们需要他们并行的进行,那么我们可以使用For或者ForEach方法来进行:

    复制代码
    ParallelLoopResult result =
                        Parallel.For(0, 100, async (int i) =>
                        {
                            Console.WriteLine("{0}, task: {1}, thread: {2}", i,
                            Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
                            await Task.Delay(10);
                           
                  });
    复制代码

    如果我们在计算的过程中,想要中断并行,我们可以把ParallelLoopState当做参数传递进去,我们就可以实现认为和中断这种循环:

    复制代码
    ParallelLoopResult result =
                        Parallel.For(0, 100, async (int i, ParallelLoopState pls) =>
                        {
                            Console.WriteLine("{0}, task: {1}, thread: {2}", i,
                            Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
                            await Task.Delay(10);
                            if (i > 5) pls.Break();
                  });
    复制代码

    需要注意的是,当我们需要中断循环的时候,由于其运行在诸多个线程之上,如果线程数多于我们设定的中断数时,上述的执行可能就不太准确。

    1. 任务并行。如果我们想要多个任务并行处理,那么我们可以使用Parallel.Invoke方法来接受Action委托数组。例如:
    static void ParallelInvoke()
    {
       Parallel.Invoke(MethodOne, MethodTwo);
    }

     参考文章:http://www.codeproject.com/Articles/996857/Asynchronous-programming-and-Threading-in-Csharp-N?bmkres=exist#_articleTop

    引用:https://www.cnblogs.com/scy251147/p/4597615.html

  • 相关阅读:
    一行代码搞定Dubbo接口调用
    测试周期内测试进度报告规范
    jq 一个强悍的json格式化查看工具
    浅析Docker容器的应用场景
    HDU 4432 Sum of divisors (水题,进制转换)
    HDU 4431 Mahjong (DFS,暴力枚举,剪枝)
    CodeForces 589B Layer Cake (暴力)
    CodeForces 589J Cleaner Robot (DFS,或BFS)
    CodeForces 589I Lottery (暴力,水题)
    CodeForces 589D Boulevard (数学,相遇)
  • 原文地址:https://www.cnblogs.com/xdanny/p/11341572.html
Copyright © 2011-2022 走看看