C#异步编程

关于异步的概述,这里引用MSDN的一段文字:

异步编程是一项关键技术,使得能够简单处理多个核心上的阻塞 I/O 和并发操作。 如果需要 I/O 绑定(例如从网络请求数据或访问数据库),则需要利用异步编程。 还可以使用 CPU 绑定代码(例如执行成本高昂的计算),对编写异步代码而言,这是一个不错的方案。

异步代码具有以下特点:

  • 等待 I/O 请求返回的同时,可通过生成处理更多请求的线程,处理更多的服务器请求。
  • 等待 I/O 请求的同时生成 UI 交互线程,并通过将长时间运行的工作转换到其他 CPU 核心,让 UI 的响应速度更快。
  • 许多较新的 .NET APIs 都是异步的。

使用异步编程,方法的调用是后台运行,并且不会阻塞调用线程。

异步编程的基础

异步编程的核心是 Task 和 Task<T> 对象,这两个对象对异步操作建模。 它们受关键字 async 和 await 的支持。 在大多数情况下模型十分简单:

对于 I/O 绑定代码,当你 await 一个操作,它将返回 async 方法中的一个 Task 或 Task<T>
对于 CPU 绑定代码,当你await 一个操作,它将在后台线程通过 Task.Run 方法启动。
await 关键字有这奇妙的作用。 它控制执行 await 的方法的调用方,且它最终允许 UI 具有响应性或服务具有灵活性。

asyncawait关键字只是编译器功能,实质是编译器会用Task类创建代码。

创建任务

我们先定义一个简单的方法,在该方法中,等待3秒之后返回一个字符串:

public static string Greeting(string name)
{
//等待3秒
Task.Delay(3000).Wait(); //Wait方法用来等待之前的任务完成
return "Hello," + name;
}

上述方法中使用了Wait()方法,该方法是一个同步方法,它使调用线程等到当前任务完成。如果当前任务尚未开始执行,则Wait()方法尝试从调度程序中删除该任务,并在当前线程上内联执行该任务。如果无法执行此操作,或者当前任务已经开始执行,则会阻止调用线程,直到任务完成。

我们使用一个简单的代码来测试一下运行效果,如下:

Stopwatch sw = new Stopwatch();
Console.WriteLine("-----开始程序-----");
//开始计时
sw.Start();
//调用方法
Console.WriteLine( AsyncDemo.Greeting("world"));
Console.WriteLine("总执行时间:" + sw.Elapsed.Seconds + "秒");
sw.Stop();
Console.WriteLine("-----结束程序-----");
Console.Read();

控制台在打印出“-----开始程序-----”后,将会花费3秒时间调用Greeting()方法,上述执行结果如下:

-----开始程序-----
Hello,world
总执行时间:3秒
-----结束程序-----

接着我们定义一个将此方法异步化的另一个方法GreetingAsync(),基于任务的异步模式,指定在异步方法名后加上Async后缀,并返回一个任务。如下:

private static Task<string> GreetingAsync(string name)
{
return Task.Run<string>(() => { return Greeting(name); });
}

上述Task<string>定义了一个返回字符串的任务,该方法返回的是一个任务,该任务返回的是一个字符串。

调用异步方法

可以使用await关键字来调用返回任务的异步方法。使用await关键字需要使用async修饰符声明的方法。

public async static void CallerWithAsync()
{
string result = await GreetingAsync("异步调用方法");
Console.WriteLine(result);
}

GreetingAsync()方法完成前,CallerWithAsync()内的其他代码不会继续执行。但是调用CallerWithAsync()方法的线程并没有阻塞,可以被重用。调用CallerWithAsync()方法如下:

Stopwatch sw = new Stopwatch();
Console.WriteLine("-----开始程序-----");
//开始计时
sw.Start();
AsyncDemo.CallerWithAsync();
Console.WriteLine("总执行时间:" + sw.Elapsed.Seconds + "秒");
sw.Stop();
Console.WriteLine("-----结束程序-----");
Console.Read();

在上述代码中,直接调用了CallerWithAsync()方法,由于外部并不会被阻塞,所以直接会执行AsyncDemo.CallerWithAsync()之后的代码,得到的结果如下:

-----开始程序-----
总执行时间:0秒
-----结束程序-----
Hello,异步调用方法

延续任务(await关键字的实质)

Task类的ContinueWith()方法定义了任务完成后就调用的代码。指派给ContinueWith()方法的委托接收将已完成的任务作为参数传入,使用Result属性可以访问任务返回的结果。

将上述使用await关键字调用的方法CallerWithAsync(),使用Task类的ContinueWith()进行实现:

public static void CallerWithContinuationTask()
{
Task<string> t1 = GreetingAsync("异步调用方法");
t1.ContinueWith(t =>
{
string result = t.Result;
Console.WriteLine(result);
});
}

C#编译器会把await关键字后的所有代码放进ContinueWith()方法的代码块中来转换await关键字。

因此该方法的执行效果和CallerWithAsync()方法的执行效果一样,它们输出结果也相同。

同步上下文

在上述的方法CallerWithAsync()中(或CallerWithContinuationTask()方法中),不同的执行阶段使用了不同的线程,一个线程用于调用CallerWithAsync()方法,我们把这个线程称作为外部调用线程或前台线程、主线程,另一个线程执行await关键字后面的代码,或者继续执行ContinueWith()方法内的代码块,我们把这个线程称为方法内部执行线程或后台线程。在使用异步时,必须保证在所有应该完成的后台任务之前,至少有一个前台线程仍然在运行。上述实例中的Console.Read()就是用来保证主线程一直在运行。

有时候,为了执行某些动作,有些应用程序会绑定到指定的线程上。例如,在winform或WPF应用程序中,只有UI线程才能访问UI元素,这将会是一个问题。在未出现asyncawait之前,需要借助委托来解决这类问题,代码相对比较繁琐。

而使用asyncawait关键字,当await完成之后,不需要进行任何特别处理,就能访问UI线程。默认情况下,生成的代码就会把线程转换到拥有同步上下文的线程中。WPF设置了DispatcherSynchronizationContext属性,winfrom设置了WindowsFormsSynchronizationContext属性。如果调用异步方法的线程分配了同步上下文,await完成之后将继续执行。默认情况下,使用了同步上下文。如果不使用相同的同步上下文,则必须调用Task方法ConfigureAwait(continueOnCapturedContext:false)。例如, 一个WPF应用程序,其await后面的代码没有用到任何的UI元素。在这种情况下,避免切换到同步上下文会执行的更快。

使用多个异步方法

在一个异步方法里,可以调用一个或多个异步方法。如何编写代码,取决于一个异步方法的结果是否依赖于另一个异步方法。

按顺序调用多个异步方法

如果某个异步方法,需要在之前的其他的异步方法执行完后才被调用,就需要使用await关键字。它可以实现一个异步方法依赖另一个异步方法的结果的情况。

public async static void MultipleAsyncMehtods()
{
string s1 =await GreetingAsync("Mul1");
string s2 =await GreetingAsync("Mul2");
Console.WriteLine("Mul:" + s1 + " " + s2);
}

使用组合器

如果异步方法不依赖于其他异步方法,比如无返回值的异步方法或返回值互不联系,可以在异步方法被调用时,不使用await关键字,而是把每个异步方法的返回结果赋值给Task变量,这样运行的就会更快一些。

使用组合器,可以同时并行运行多个异步方法。一个组合器可以接受多个同一类型的参数,并返回同一类型的值。多个同一类型的参数被组合成一个参数来传递。Task组合器接受多个Task对象作为参数,并返回一个Task

下述示例中使用Task.WhenAll组合器方法,它可以等待,直到两个任务都完成:

public async static void MultipleAsyncMethodsWithCombinators1()
{
Task<string> t1 = GreetingAsync("mulA");
Task<string> t2 = GreetingAsync("mulB");
await Task.WhenAll(t1, t2);
Console.WriteLine("结果:" + t1.Result + " " + t2.Result);
}

Task类型的WhenAll方法定义了多个重载版本,如果所有的任务返回相同的类型,可以使用该类型的数组接收await返回的结果。如下:

private async static void MultipleAsyncMethodsWithCombinators2()
{
Task<string> t1 = GreetingAsync("mulA");
Task<string> t2 = GreetingAsync("mulB");
string[] result = await Task.WhenAll(t1, t2);
Console.WriteLine("结果:" + result[0] + " " + result[1]);
}

除了WhenAll组合器外,Task类还定义了WhenAny组合器。

  • WhenAll:从WhenAll方法返回的Task,是在所有传入方法的任务都完成了才会返回Task
  • WhenAny:从WhenAny返回的Task,是在其中一个传入方法的任务完成了就会返回Task

转换异步模式

并非.NET Framework的所有类都引入了新的异步方法,有些类只提供了BeginXXX方法和EndXXX方法的异步模式,没有提供基于任务的异步模式。我们可以把这些旧的异步模式转换为新的基于任务的异步模式。

为了模拟出BeginXXX和EndXXX这种形式的方法,对之前的同步方法Greeting()进行扩展:

public static string Greeting(string name)
{
//等待3秒
Task.Delay(3000).Wait(); //Wait方法用来等待之前的任务完成
return "Hello," + name;
}
//定义一个委托
private static Func<string, string> greetingInvoker = Greeting; //模拟异步模式
private static IAsyncResult BeginGreeting(
string name,AsyncCallback callback, object state)
{
return greetingInvoker.BeginInvoke(name, callback, state);
}
//该方法返回来自于Greeting的结果
private static string EndGreeting(IAsyncResult ar)
{
return greetingInvoker.EndInvoke(ar);
}
//使用新的基于任务的异步模式进行调用
public static async void ConvertingAsyncPattern()
{
string s = await Task<string>.Factory.FromAsync<string>(
BeginGreeting, EndGreeting, "测试", null);
Console.WriteLine(s);
}

上述实例中,使用TaskFactory类的FromAsync()方法,把使用旧的异步模式的方法转换为基于任务的异步模式的方法(TAP)。其中,Task类型的第一个泛型参数Task<string>定义了调用方法的返回值类型。FromAsync()方法的泛型参数定义了方法的输入类型。FromAsync()方法的前两个参数是委托类型,传入BeginGreetingEndGreeting方法的声明。紧跟这两个参数后面的是输入参数和对象状态参数。因对象状态没有用到,所以分配null值。因为FromAsync()方法返回Task类型,可以使用await修饰。

错误处理

为了说明多个异步方法出现错误时的异常处理情况,我们先定义一个简单的方法,如下:

//注:该方法不是最终解决方案,终极方法见下述说明
public static async void ThrowAfter(int ms, string message)
{
await Task.Delay(ms);
throw new Exception(message);
}

上述方法将在指定的时间间隔抛出一个异常。如果直接在try/catch块中调用该异步方法,并且没有等待,就会捕获不到异常,代码如下:

//注:该方法不是最终解决方案,终极方法见下述说明
public static void DontHandle()
{
try
{
ThrowAfter(200, "first");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

上述的代码并不能捕获到任何异常,这是因为DontHandle()方法在ThrowAfter()抛出异常之前,就已经执行完毕。正确的做法是使用await关键字等待ThrowAfter()方法执行完才能捕获。由于ThrowAfter()是一个void方法,返回void的异步方法不能使用await关键字进行等待,就无法捕获异常,因此异常方法最好返回一个Task类型。对上述方法进行重构:

//注:终极方法
public async static Task ThrowAfter(int ms, string message)
{
await Task.Delay(ms);
throw new Exception(message);
}
//注:终极方法
public async static void DontHandle()
{
try
{
await ThrowAfter(200, "first");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

重构后的方法可以正常的捕获抛出的异常信息。

多个异步方法的异常处理

上述示例针对单一的异步方法比较容易捕获,如果是多个异步方法,使用上述这种做法并不能够捕获全部的异常。

例如:

//注:该方法不是最终解决方案,终极方法见下述说明
public static async void StartTwoTask()
{
try
{
await ThrowAfter(2000, "first");
await ThrowAfter(1000, "second");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

上述代码中,在第一个ThrowAfter()方法抛出异常后,try/catch代码块就会捕获到异常,直接跳过第二个ThrowAfter()方法的执行,因此本示例只能捕获第一个方法抛出的异常,并不能够捕获第二次抛出的异常。

如果采用Task.WhenAll()方法并行的调用这两个ThrowAfter()方法,代码如下:

//注:该方法不是最终解决方案,终极方法见下述说明
public async static void StartTwoTaskParallel()
{
try
{
Task t1 = ThrowAfter(2000, "first");
Task t2 = ThrowAfter(1000, "second");
await Task.WhenAll(t1, t2);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

使用Task.WhenAll,不管任务是否抛出异常,都会等到两个任务完成。但是,如果只是单纯的使用Task.WhenAll,实际上并不能捕获所有的异常,上述代码只能捕获第一次调用抛出的异常,并不能捕获第二次抛出的异常。为了捕获所有的异常,需要结合使用AggregateException类型。

使用AggregateException信息捕获所有异常

并行调用异步方法捕获异常的终极解决方案如下:

//注:终极方案代码
public static async void ShowAggregatedException()
{
Task taskResult = null;
try
{
Task t1 = ThrowAfter(2000, "first");
Task t2 = ThrowAfter(1000, "second");
await (taskResult = Task.WhenAll(t1, t2));
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
foreach(var ex1 in taskResult.Exception.InnerExceptions)
{
Console.WriteLine(ex1.Message);
}
}
}

通过外部任务的Exception属性,Exception属性是AggregateException类型的,这个类型定义了InnerExceptions属性,它包含了等待中的所有异常的列表,通过遍历列表,可以捕获每一次任务的异常信息。

取消任务

取消任务常常应用于长时间运行的后台任务。对于取消任务,.NET提供了一种标准的机制。这种机制可用于基于任务的异步模式。

取消框架基于协助行为,不是强制性的。一个运行时间很长的任务需要检查自己是否被取消,在这种情况下,它的工作就是清理所有已打开的资源,并结束相关工作。

取消基于CancellationTokenSource类,该类用于发送取消请求。请求发送给引用CancellationToken结构类的任务,其中CancellationToken结构与CancellationTokenSource类相关联。

CancellationTokenSource类还支持在指定时间后才取消任务。CancelAfter方法传入一个时间值,单位是毫秒,在该时间过后,就取消任务。

可以将CancellationToken传入异步方法,框架中的某些异步方法提供可以传入CancellationToken的重载版本,来支持取消任务。一旦取消,就会清理资源,之后抛出异常。

注:取消任务之后,都会抛出异常,可以通过调试的方式,在catch块中进行捕获对应的异常信息。

个人总结

  • await关键字用来修饰的是返回Task[<T>]的方法,而不是一个返回普通类型的方法,并不是方法名带有async就一定要使用await关键字修饰,需要根据该方法返回的类型进行确定。
  • 在方法的内部使用了await关键字的方法,必须使用async关键字进行修饰。
  • 使用了async关键字修饰的方法,在被主线程调用时,该主线程并不会受方法内部的await关键字的影响,不会被阻塞,依然会运行。而方法内部使用await修饰的代码,在未完成前,其他代码不会被执行。

C#基础提升系列——C#异步编程的更多相关文章

  1. Boost.Asio基础(五) 异步编程初探

    异步编程 本节深入讨论异步编程将遇到的若干问题.建议多次阅读,以便吃透这一节的内容,这一节是对整个boost.asio来说是非常重要的. 为什么须要异步 如前所述,通常同步编程要比异步编程更简单.同步 ...

  2. C#基础提升系列——C#任务和并行编程

    C#任务和并行编程 我们在处理有些需要等待的操作时,例如,文件读取.数据库或网络访问等,这些都需要一定的时间,我们可以使用多线程,不需要让用户一直等待这些任务的完成,就可以同时执行其他的一些操作.即使 ...

  3. C#基础提升系列——C#文件和流

    C#文件和流 本文主要是对C#中的流进行详细讲解,关于C#中的文件操作,考虑到后期.net core跨平台,相关操作可能会发生很大变化,所以此处不对文件系统(包括目录.文件)过多的讲解,只会描述出在. ...

  4. C#基础提升系列——C#任务同步

    C#任务同步 如果需要共享数据,就必须使用同步技术,确保一次只有一个线程访问和改变共享状态.如果不注意同步,就会出现争用条件和死锁. 不同步导致的线程问题 如果两个或多个线程访问相同的对象,并且对共享 ...

  5. C#基础提升系列——C#委托

    C# 委托 委托是类型安全的类,它定义了返回类型和参数的类型,委托类可以包含一个或多个方法的引用.可以使用lambda表达式实现参数是委托类型的方法. 委托 当需要把一个方法作为参数传递给另一个方法时 ...

  6. C#基础提升系列——C# 泛型

    C# 泛型(Generics) 泛型概述 泛型是C#编程语言的一部分,它与程序集中的IL(Intermediate Language,中间语言)代码紧密的集成.通过泛型,我们不必给不同的类型编写功能相 ...

  7. C#基础提升系列——C# LINQ

    C# LINQ LINQ(Language Integrated Query,语言集成查询).在C# 语言中集成了查询语法,可以用相同的语法访问不同的数据源. 命名空间System.Linq下的类En ...

  8. C#基础提升系列——C#集合

    C#集合 有两种主要的集合类型:泛型集合和非泛型集合. 泛型集合被添加在 .NET Framework 2.0 中,并提供编译时类型安全的集合. 因此,泛型集合通常能提供更好的性能. 构造泛型集合时, ...

  9. Javascript异步编程之一异步原理

    本系列的例子主要针对node.js环境,但浏览器端的原理应该也是类似的. 本人也是Javascript新手,把自己这段时间学习积累的要点总结下来,希望可以对同样在学习Javascript/node.j ...

随机推荐

  1. Jenkins报错:该Jenkins实例似乎已离线

    解决方法: 1.保留此离线页面,重新开启一个浏览器tab标签页 2.输入输入网址http://localhost:8080/pluginManager/advanced 3.进入该页面最底下,找到[升 ...

  2. Appium解决native+webview混合型APP(公众号、小程序)切换webview后元素无法定位问题

    问题:最近在做一个安卓+H5混合开发的APP自动化测试,发现在从native切换到webview后,元素仍然无法找到,报错:no such element 思路:于是思考webview会不会像web页 ...

  3. 计算一段日期内的周末天数的php代码(星期六,星期日总和)

    代码如下: /*| Author: Yang Yu <niceses@gmail.com>| @param char|int $start_date 一个有效的日期格式,例如:200910 ...

  4. [CSP-S模拟测试]:柱状图(树状数组+二分+三分)

    题目描述 $WTH$获得了一个柱状图,这个柱状图一共有$N$个柱子,最开始第$i$根柱子的高度为$x_i$,他现在要将这个柱状图排成一个屋顶的形状,屋顶的定义如下:$1.$屋顶存在一个最高的柱子,假设 ...

  5. Spring Boot 的单元测试

    Spring Boot 的单元测试 引入依赖 testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-tes ...

  6. php面试专题---16、MySQL创建高性能索引考点

    php面试专题---16.MySQL创建高性能索引考点 一.总结 一句话总结: 注意:只写精品 1.索引的基础? 类似书籍的目录:索引类似于书籍的目录,要想找到一本书的某个特定主题,需要先查找书的目录 ...

  7. php面试专题---Mysql索引原理及SQL优化

    php面试专题---Mysql索引原理及SQL优化 一.总结 一句话总结: 注意:只写精品 1.为表设置索引要付出代价 是什么? 存储空间:一是增加了数据库的存储空间 修改插入变动索引时间:二是在插入 ...

  8. Fedora 26 安装搜狗拼音输入法 sogoupinyin

    Fcitx 官方 wiki Fcitx GitHub Arch wiki 上关于 Fcitx 的资料 故障排除可以参考这里 Fedora 中文社区 Fedora 中文社区软件源 Fcitx (Flex ...

  9. python selenium无法清除文本框内容问题

    正常是我们在清除文本框内容的时候,都会使用 clear() 函数进行清除,但是有时候会出现,清除完成后再点击查询时,文本框的内容会再次自动填充,这个时候我们可以选择以下方式: #清空查询条件drive ...

  10. Linux grep常用命令

    在一个文件中同时查找多个字符串: 并集语法: grep -e 'pattern1 -e 'pattern2 file 或集语法: 1.grep -E 'pattern1|pattern2' file ...