• 异步编程(async&await)


     前言

    本来这篇文章上个月就该发布了,但是因为忙 QuarkDoc  一直没有时间整理,所以耽搁到今天,现在回归正轨。

    C# 5.0 虽然只引入了2个新关键词:asyncawait。然而它大大简化了异步方法的编程。

    在 线程池(threadPool)大致介绍了微软在不同时期使用的不同的异步模式,有3种:

    1.异步模式

    2.基于事件的异步模式

    3.基于任务的异步模式(TAP)

    而最后一种就是利用asyncawait关键字来实现的(TAP是现在微软极力推崇的一种异步编程方式)。

    但请谨记,asyncawait关键字只是编译器功能。编译器会用Task类创建代码。如果不使用这两个关键词,用C#4.0的Task类同样可以实现相同的功能,只是没有那么方便而已。

    认识asyncawait

    使用asyncawait关键词编写异步代码,具有与同步代码相当的结构和简单性,并且摒弃了异步编程的复杂结构。

    但是在理解上刚开始会很不习惯,而且会把一些情况想当然了,而真实情况会相去甚远(我犯过这样的错误)。所以根据几个示例一步步理解更加的靠谱些。

    1.一个简单的同步方法

    这是一个简单的同步方法调用示例:

     1     class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             Console.WriteLine($"头部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
     6             string result = SayHi("jack");
     7             Console.WriteLine(result);
     8             Console.WriteLine($"尾部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
     9             Console.ReadKey();
    10         }
    11         static string SayHi(string name)
    12         {
    13             Task.Delay(2000).Wait();//异步等待2s
    14             Console.WriteLine($"SayHi执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
    15             return $"Hello,{name}";
    16         }
    17     }

    执行结果如下,方法在主线程中运行,主线程被阻塞。

    2.同步方法异步化

    示例将方法放到任务内执行:

     1     class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             Console.WriteLine($"头部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
     6             string result = SayHiAsync("jack").Result;
     7             Console.WriteLine(result);
     8             Console.WriteLine($"尾部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
     9             Console.ReadKey();
    10         }
    11         static Task<string> SayHiAsync(string name)
    12         {
    13             return Task.Run<string>(() => { return SayHi(name); });
    14         }
    15         static string SayHi(string name)
    16         {
    17             Task.Delay(2000).Wait();//异步等待2s
    18             Console.WriteLine($"SayHi执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
    19             return $"Hello,{name}";
    20         }
    21     }

    执行结果如下,方法在另外一个线程中运行,因为主线程调用了ResultResult在任务没有完成时内部会使用Wait,所以主线程还是会被阻塞。

    3.延续任务

    示例为了避免阻塞主线程使用任务延续的方式:

     1     class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             Console.WriteLine($"头部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
     6             Task<string> task = SayHiAsync("jack");
     7             task.ContinueWith(t =>//延续任务,指定任务执行完成后延续的操作
     8             {
     9                 Console.WriteLine($"延续执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
    10                 string result = t.Result;
    11                 Console.WriteLine(result);
    12             });
    13             Console.WriteLine($"尾部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
    14             Console.ReadKey();
    15         }
    16         static Task<string> SayHiAsync(string name)
    17         {
    18             return Task.Run<string>(() => { return SayHi(name); });
    19         }
    20         static string SayHi(string name)
    21         {
    22             Task.Delay(2000).Wait();//异步等待2s
    23             Console.WriteLine($"SayHi执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
    24             return $"Hello,{name}";
    25         }
    26     }

    执行结果如下,方法在另外一个线程中运行,因为任务附加了延续,延续会在任务完成后处理返回值,而主线程不会被阻塞。这应该就是想要的效果了。

     

    4.使用async和await构建异步方法调用

     1     class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             Console.WriteLine($"头部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
     6             CallerWithAsync("jack");
     7             Console.WriteLine($"尾部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
     8             Console.ReadKey();
     9         }
    10         async static void CallerWithAsync(string name)
    11         {
    12             Console.WriteLine($"异步调用头部执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
    13             string result = await SayHiAsync(name);
    14             Console.WriteLine($"异步调用尾部执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
    15             Console.WriteLine(result);
    16         }
    17         static Task<string> SayHiAsync(string name)
    18         {
    19             return Task.Run<string>(() => { return SayHi(name); });
    20         }
    21         static string SayHi(string name)
    22         {
    23             Task.Delay(2000).Wait();//异步等待2s
    24             Console.WriteLine($"SayHi执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
    25             return $"Hello,{name}";
    26         }
    27     }

    执行结果如下,使用await关键字来调用返回任务的异步方法SayHiAsync,而使用await需要有用async修饰符声明的方法,在SayHiAsync方法为完成前,下面的方法不会继续执行。但是主线程并没有阻塞,且任务处理完成后await后的逻辑继续执行。

    本质:编译器将await关键字后的所有代码放进了延续(ContinueWith)方法的代码块中来转换await关键词。

    解析asyncawait

    1.异步(async)

    使用async修饰符标记的方法称为异步方法,异步方法只可以具有以下返回类型:
    1.Task
    2.Task<TResult>
    3.void
    4.从C# 7.0开始,任何具有可访问的GetAwaiter方法的类型System.Threading.Tasks.ValueTask<TResult> 类型属于此类实现(需向项目添加System.Threading.Tasks.Extensions NuGet 包)。

    异步方法通常包含 await 运算符的一个或多个实例,但缺少 await 表达式也不会导致生成编译器错误。 如果异步方法未使用 await 运算符标记暂停点,那么异步方法会作为同步方法执行,即使有 async 修饰符也不例外,编译器将为此类方法发布一个警告。

    2.等待(await)

    await 表达式只能在由 async 修饰符标记的封闭方法体lambda 表达式或异步方法中出现。在其他位置,它会解释为标识符。

    使用await运算符的任务只可用于返回 TaskTask<TResult> System.Threading.Tasks.ValueType<TResult> 对象的方法。

    异步方法同步运行,直至到达其第一个 await 表达式,此时await在方法的执行中插入挂起点,会将方法挂起直到所等待的任务完成,然后继续执行await后面的代码区域。

    await 表达式并不阻止正在执行它的线程。 而是使编译器将剩下的异步方法注册为等待任务的延续任务。 控制权随后会返回给异步方法的调用方。 任务完成时,它会调用其延续任务,异步方法的执行会在暂停的位置处恢复。

    注意:

    1.无法等待具有 void 返回类型的异步方法,并且无效返回方法的调用方捕获不到异步方法抛出的任何异常

    2.异步方法无法声明 inrefout 参数,但可以调用包含此类参数的方法。 同样,异步方法无法通过引用返回值,但可以调用包含 ref 返回值的方法。

    异步方法运行机理(控制流)

    异步编程中最需弄清的是控制流是如何从方法移动到方法的。

    下列示例及说明引自(官方文档),个人认为已经很清晰了:

     1     class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             var result = AccessTheWebAsync();
     6             Console.ReadKey();
     7         }
     8         async static Task<int> AccessTheWebAsync()
     9         {
    10             HttpClient client = new HttpClient();
    11             // GetStringAsync返回一个任务。任务Result会得到一个字符串(urlContents)。
    12             Task<string> getStringTask = client.GetStringAsync("https://www.cnblogs.com/jonins/");
    13             //您可以在这里完成不依赖于GetStringAsync的字符串的工作。
    14             DoIndependentWork();
    15             //等待的操作员暂停进入WebAsync。
    16             //AccessTheWebAsync在getStringTask完成之前不能继续。
    17             //同时,控制权返回到AccessTheWebAsync的调用方。
    18             //当getStringTask完成后,控件权将继续在这里工作。 然后,await运算符从getStringTask检索字符串结果。 
    19             string urlContents = await getStringTask;
    20             //任务完成
    21             Console.WriteLine(urlContents.Length);
    22             //return语句指定一个整数结果。 
    23             return urlContents.Length;
    24         }
    25         static void DoIndependentWork()
    26         {
    27             Console.WriteLine("Working..........");
    28         }
    29     }

    多个异步方法

    在一个异步方法里,可以调用一个或多个异步方法,如何编码取决于异步方法间结果是否相互依赖

    1.顺序调用异步方法

    使用await关键词可以调用每个异步方法,如果一个异步方法需要使用另一个异步方法的结果,await关键词就非常必要。

    示例如下:

     1     class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             Console.WriteLine("执行前.....");
     6             GetResultAsync();
     7             Console.WriteLine("执行中.....");
     8             Console.ReadKey();
     9         }
    10         async static void GetResultAsync()
    11         {
    12             var number1 = await GetResult(10);
    13             var number2 =  GetResult(number1);
    14             Console.WriteLine($"结果分别为:{number1}和{number2.Result}");
    15         }
    16         static Task<int> GetResult(int number)
    17         {
    18             return Task.Run<int>(() => { Task.Delay(1000).Wait(); return number + 10; });
    19         }
    20     }

    2.使用组合器

    如果异步方法间相互不依赖,则每个异步方法都不使用await,而是把每个异步方法的结果赋值给Task变量,就会运行得更快。

    示例如下:

     1     class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             Console.WriteLine("执行前.....");
     6             GetResultAsync();
     7             Console.WriteLine("执行中.....");
     8             Console.ReadKey();
     9         }
    10         async static void GetResultAsync()
    11         {
    12             Task<int> task1 = GetResult(10);
    13             Task<int> task2 = GetResult(20);
    14             await Task.WhenAll(task1, task2);
    15             Console.WriteLine($"结果分别为:{task1.Result}和{task2.Result}");
    16         }
    17         static Task<int> GetResult(int number)
    18         {
    19             return Task.Run<int>(() => { Task.Delay(1000).Wait(); return number + 10; });
    20         }
    21     }

    Task类定于2个组合器分别为:WhenAllWhenAny

    WhenAll是在所有传入的任务都完成时才返回Task

    WhenAny是在传入的任务其中一个完成就会返回Task

    异步方法的异常处理

    1.异常处理

    以下示例一种是普通的错误的捕获方式,另一种是异步方法异常捕获方式:

     1     class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5 
     6             DontHandle();
     7             HandleError();
     8             Console.ReadKey();
     9         }
    10         //错误处理
    11         static void DontHandle()
    12         {
    13             try
    14             {
    15                 var task = ThrowAfter(0, "DontHandle Error");
    16             }
    17             catch (Exception ex)
    18             {
    19 
    20                 Console.WriteLine(ex.Message);
    21             }
    22         }
    23         //异步方法错误处理
    24         static async void HandleError()
    25         {
    26             try
    27             {
    28                 await ThrowAfter(2000, "HandleError Error");
    29             }
    30             catch (Exception ex)
    31             {
    32 
    33                 Console.WriteLine(ex.Message);
    34             }
    35         }
    36         //在延迟后抛出异常
    37         static async Task ThrowAfter(int ms, string message)
    38         {
    39             await Task.Delay(ms);
    40             throw new Exception(message);
    41         }
    42     }

    执行结果如下:

    调用异步方法,如果只是简单的放在try/catch块中,将会捕获不到异常这是因为DontHandle方法在ThrowAfter抛出异常之前已经执行完毕(返回void的异步方法不会等待。这是因为从async void方法抛出的异常无法捕获。因此异步方法最好返回一个Task类型)。

    异步方法的一个较好异常处理方式是使用await关键字,将其放在try/catch

    2.多个异步方法异常处理

    如果调用了多个异步方法,在第一个异步方法抛出异常,后续的方法将不会被调用,catch块内只会处理出现的第一个异常。

    所以正确的做法是使用Task.WhenAll,不管任务是否抛出异常都会等到所有任务完成。Task.WhenAll结束后,异常被catch语句捕获到。如果只是捕获Exception,我们只能看到WhenAll方法的第一个发生异常的任务信息,不会抛出后续的异常任务

    如果要捕获所有任务的异常信息,就是对任务声明变量,在catch块内可以访问,再使用IsFaulted属性检查任务的状态,以确认它们是否出现错误,然后再进行处理。示例如下:

     1     class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             HandleError();
     6             Console.ReadKey();
     7         }
     8         //正确的处理方式
     9         static async void HandleError()
    10         {
    11             Task t1 = null;
    12             Task t2 = null;
    13             try
    14             {
    15                 t1 = ThrowAfter(1000, "HandleError-One-Error");
    16                 t2 = ThrowAfter(2000, "HandleError-Two-Error");
    17                 await Task.WhenAll(t1, t2);
    18             }
    19             catch (Exception)
    20             {
    21                 if (t1.IsFaulted)
    22                     Console.WriteLine(t1.Exception.InnerException.Message);
    23                 if (t2.IsFaulted)
    24                     Console.WriteLine(t2.Exception.InnerException.Message);
    25             }
    26         }
    27         //在延迟后抛出异常
    28         static async Task ThrowAfter(int ms, string message)
    29         {
    30             await Task.Delay(ms);
    31             throw new Exception(message);
    32         }
    33     }

    3.使用AggregateException捕获异步方法异常

     在 任务(task)中介绍过AggregateException,它包含了等待中所有异常的列表,可轻松遍历处理所有异常信息。示例如下:

     1     class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             HandleError();
     6             Console.ReadKey();
     7         }
     8         //正确的处理方式
     9         static async void HandleError()
    10         {
    11             Task taskResult = null;
    12             try
    13             {
    14                 Task t1 = ThrowAfter(1000, "HandleError-One-Error");
    15                 Task t2 = ThrowAfter(2000, "HandleError-Two-Error");
    16                 await (taskResult = Task.WhenAll(t1, t2));
    17             }
    18             catch (Exception)
    19             {
    20                 foreach (var ex in taskResult.Exception.InnerExceptions)
    21                 {
    22                     Console.WriteLine(ex.Message);
    23                 }
    24           
    25             }
    26         }
    27         //在延迟后抛出异常
    28         static async Task ThrowAfter(int ms, string message)
    29         {
    30             await Task.Delay(ms);
    31             throw new Exception(message);
    32         }
    33     }

    重要的补充与建议

    1.提高响应能力

    .NET有很多异步API我们都可以通过async/await构建调用提高响应能力,例如:

     1     class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             Demo();
     6             Console.ReadKey();
     7         }
     8         static async void Demo()
     9         {
    10             HttpClient httpClient = new HttpClient();
    11             var getTaskResult = await httpClient.GetStringAsync("https://www.cnblogs.com/jonins/");
    12             Console.WriteLine(getTaskResult);
    13         }
    14     }

    这些API都有相同原则即以Async结尾。

     

    2.重要建议

    1.async方法需在其主体中具有await 关键字,否则它们将永不暂停。同时C# 编译器将生成一个警告,此代码将会以类似普通方法的方式进行编译和运行。 请注意这会导致效率低下,因为由 C# 编译器为异步方法生成的状态机将不会完成任何任务。

    2.应将“Async”作为后缀添加到所编写的每个异步方法名称中。这是 .NET 中的惯例,以便更轻松区分同步和异步方法。

    3.async void 应仅用于事件处理程序。因为事件不具有返回类型(因此无法返回 TaskTask<T>)。 其他任何对 async void 的使用都不遵循 TAP 模型,且可能存在一定使用难度。

    例如:async void 方法中引发的异常无法在该方法外部被捕获或十分难以测试 async void 方法。

    3.以非阻止方式处理等待任务

     

    异步编程准则

    异步编程的准则确定所需执行的操作是I/O-Bound还是 CPU-Bound。因为这会极大影响代码性能,并可能导致某些构造的误用。

    考虑两个问题:

    1.你的代码是否会“等待”某些内容,例如数据库中的数据或web资源等?如果答案为“是”,则你的工作是 I/O-Bound

    2.你的代码是否要执行开销巨大的计算?如果答案为“是”,则你的工作是 CPU-Bound

    如果你的工作为 I/O-Bound,请使用 async await(而不使用 Task.Run)。 不应使用任务并行库。 
    如果你的工作为 CPU-Bound,并且你重视响应能力,请使用 async await,并在另一个线程上使用 Task.Run 生成工作。 如果该工作同时适用于并发和并行,则应考虑使用任务并行库。

    结语

    如果想要了解状态机请戳:这里 。

    参考资料

    C#高级编程(第10版) C# 6 & .NET Core 1.0   Christian Nagel  

    果壳中的C# C#5.0权威指南  Joseph Albahari

    https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/index

    https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/async

    https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/await

    作者:Jonins    出处:http://www.cnblogs.com/jonins/
     
    个人原创,若有错误或补充请联系作者。
     
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    原型模式&原型链
    [四种方法]检测数据类型
    JSON
    PHP基础 mysqli的事务处理
    PHP的扩展类 mysqli_stmt:预处理类
    PHP的 Mysqli扩展库的多语句执行
    PHP基础文件下载类的简单封装
    PHP基础封装简单的MysqliHelper类
    Spring深入浅出(四)AOP面向切面
    Spring深入浅出(二)IOC的单例 ,继承,依赖,JDBC,工厂模式以及自动装载
  • 原文地址:https://www.cnblogs.com/jonins/p/9558275.html
Copyright © 2020-2023  润新知