c# 异步编程基础讲解

现代应用程序广泛使用文件和网络 I/O。I/O 相关 API 传统上默认是阻塞的,导致用户体验和硬件利用率不佳,此类问题的学习和编码的难度也较大。而今基于 Task 的异步 API 和语言级异步编程模式颠覆了传统模式,使得异步编程非常简单,几乎没有新的概念需要学习。

异步代码有如下特点:

  • 在等待 I/O 请求返回的过程中,通过让出线程来处理更多的服务器请求。
  • 通过在等待 I/O 请求时让出线程进行 UI 交互,并将长期运行的工作过渡到其他 CPU,使用户界面的响应性更强。
  • 许多较新的 .NET API 都是异步的。
  • 在 .NET 中编写异步代码很容易。

使用 .NET 基于 Task 的异步模型可以直接编写 I/O 和 CPU 受限的异步代码。该模型围绕着Task和Task<T>类型以及 C# 的async和await关键字展开。本文将讲解如何使用 .NET 异步编程及一些相关基础知识。

Task 和 Task<T>

Task 是 Promise 模型的实现。简单说,它给出“承诺”:会在稍后完成工作。而 .NET 的 Task 是为了简化使用“承诺”而设计的 API。

Task 表示不返回值的操作, Task<T> 表示返回T类型的值的操作。

重要的是要把 Task 理解为发起异步工作的抽象,而不是对线程的抽象。默认情况下,Task 在当前线程上执行,并酌情将工作委托给操作系统。可以选择通过Task.RunAPI 明确要求任务在单独的线程上运行。

Task 提供了一个 API 协议,用于监视、等待和访问任务的结果值。比如,通过await关键字等待任务执行完成,为使用 Task 提供了更高层次的抽象。

使用 await 允许你在任务运行期间执行其它有用的工作,将控制权交给其调用者,直到任务完成。你不再需要依赖回调或事件来在任务完成后继续执行后续工作。

I/O 受限异步操作

下面示例代码演示了一个典型的异步 I/O 调用操作:

public Task<string> GetHtmlAsync()
{
    // 此处是同步执行
    var client = new HttpClient();
    return client.GetStringAsync("https://www.dotnetfoundation.org");
}

这个例子调用了一个异步方法,并返回了一个活动的 Task,它很可能还没有完成。

下面第二个代码示例增加了async和await关键字对任务进行操作:

public async Task<string> GetFirstCharactersCountAsync(string url, int count)
{
    // 此处是同步执行
    var client = new HttpClient();

    // 此处 await 挂起代码的执行,把控制权交出去(线程可以去做别的事情)
    var page = await client.GetStringAsync("https://www.dotnetfoundation.org");

    // 任务完成后恢复了控制权,继续执行后续代码
    // 此处回到了同步执行

    if (count > page.Length)
    {
        return page;
    }
    else
    {
        return page.Substring(0, count);
    }
}

使用 await 关键字告诉当前上下文赶紧生成快照并交出控制权,异步任务执行完成后会带着返回值去线程池排队等待可用线程,等到可用线程后,恢复上下文,线程继续执行后续代码。

GetStringAsync() 方法的内部通过底层 .NET 库调用资源(也许会调用其他异步方法),一直到 P/Invoke 互操作调用本地(Native)网络库。本地库随后可能会调用到一个系统 API(如 Linux 上 Socket 的write()API)。Task 对象将通过层层传递,最终返回给初始调用者。

在整个过程中,关键的一点是,没有一个线程是专门用来处理任务的。虽然工作是在某种上下文中执行的(操作系统确实要把数据传递给设备驱动程序并中断响应),但没有线程专门用来等待请求的数据回返回。这使得系统可以处理更大的工作量,而不是干等着某个 I/O 调用完成。

虽然上面的工作看似很多,但与实际 I/O 工作所需的时间相比,简直微不足道。用一条不太精确的时间线来表示,大概是这样的:

0-1--------------------2-3

从0到1所花费的时间是await交出控制权之前所花的时间。从1到2花费的时间是GetStringAsync方法花费在 I/O 上的时间,没有 CPU 成本。最后,从2到3花费的时间是上下文重新获取控制权后继续执行的时间。

CPU 受限异步操作

CPU 受限的异步代码与 I/O 受限的异步代码有些不同。因为工作是在 CPU 上完成的,所以没有办法绕开专门的线程来进行计算。使用 async 和 await 只是为你提供了一种干净的方式来与后台线程进行交互。请注意,这并不能为共享数据提供加锁保护,如果你正在使用共享数据,仍然需要使用适当的同步策略。

下面是一个 CPU 受限的异步调用:

public async Task<int> CalculateResult(InputData data)
{
    // 在线程池排队获取线程来处理任务
    var expensiveResultTask = Task.Run(() => DoExpensiveCalculation(data));

    // 此时此处,你可以并行地处理其它工作

    var result = await expensiveResultTask;

    return result;
}

CalculateResult方法在它被调用的线程(一般可以定义为主线程)上执行。当它调用Task.Run时,会在线程池上排队执行 CPU 受限操作 DoExpensiveCalculation,并接收一个Task<int>句柄。DoExpensiveCalculation会在下一个可用的线程上并行运行,很可能是在另一个 CPU 核上。和 I/O 受限异步调用一样,一旦遇到await,CalculateResult的控制权就会被交给它的调用者,这样在DoExpensiveCalculation返回结果的时候,结果就会被安排在主线程上排队运行。

对于开发者,CUP 受限和 I/O 受限的在调用方式上没什么区别。区别在于所调用资源性质的不同,不必关心底层对不同资源的调用的具体逻辑。编写代码需要考虑的是,对于 CUP 受限的异步任务,根据实际情况考虑是否需要使其和其它任务并行执行,以加快程序的整体运行时间。

异步编程模式

最后简单回顾一下 .NET 历史上提供的三种执行异步操作的模式。

  • 基于任务的异步模式(Task-based Asynchronous Pattern,TAP),它使用单一的方法来表示异步操作的启动和完成。TAP 是在 .NET Framework 4 中引入的。它是 .NET 中异步编程的推荐方法。C# 中的 async 和 await 关键字为 TAP 添加了语言支持。
  • 基于事件的异步模式(Event-based Asynchronous Pattern,EAP),这是基于事件的传统模式,用于提供异步行为。它需要一个具有 Async 后缀的方法和一个或多个事件。EAP 是在 .NET Framework 2.0 中引入的。它不再被推荐用于新的开发。
  • 异步编程模式(Asynchronous Programming Model,APM)模式,也称为 IAsyncResult 模式,这是使用 IAsyncResult 接口提供异步行为的传统模式。在这种模式中,需要Begin和End方法同步操作(例如,BeginWrite和EndWrite来实现异步写操作)。这种模式也不再推荐用于新的开发。

下面简单举例对三种模式进行比较。

假设有一个 Read 方法,该方法从指定的偏移量开始将指定数量的数据读入提供的缓冲区:

public class MyClass
{
    public int Read(byte [] buffer, int offset, int count);
}

若用 TAP 异步模式来改写,该方法将是简单的一个 ReadAsync 方法:

public class MyClass
{
    public Task<int> ReadAsync(byte [] buffer, int offset, int count);
}

若使用 EAP 异步模式,需要额外多定义一些类型和成员:

public class MyClass
{
    public void ReadAsync(byte [] buffer, int offset, int count);
    public event ReadCompletedEventHandler ReadCompleted;
}

public delegate void ReadCompletedEventHandler(
    object sender, ReadCompletedEventArgs e);

public class ReadCompletedEventArgs : AsyncCompletedEventArgs
{
    public MyReturnType Result { get; }
}

若使用 AMP 异步模式,则需要定义两个方法,一个用于开始执行异步操作,一个用于接收异步操作结果:

public class MyClass
{
    public IAsyncResult BeginRead(
        byte [] buffer, int offset, int count,
        AsyncCallback callback, object state);
    public int EndRead(IAsyncResult asyncResult);
}

后两种异步模式已经过时不推荐使用了,这里也不再继续探讨。岁数大点的 .NET 程序员可能比较熟悉后两种异步模式,毕竟那时候没有 async/await,应该没少折腾。

以上就是c# 异步编程基础讲解的详细内容,更多关于c# 异步编程的资料请关注我们其它相关文章!

(0)

相关推荐

  • 基于c# Task自己动手写个异步IO函数

    前言 对于服务端,达到高性能.高扩展离不开异步.对于客户端,函数执行时间是1毫秒还是100毫秒差别不大,没必要为这一点点时间煞费苦心.对于异步,好多人还有误解,如: 异步就是多线程:异步就是如何利用好线程池.异步不是这么简单,否则微软没必要在异步上花费这么多心思.本文就介绍异步最新的实现方式:Task,并自己动手写一个异步IO函数.只有了解了异步函数内部实现方式,才能更好的利用它. 对于c#,异步处理经过了多个阶段,但是对于现阶段异步就是Task,微软用Task来抽象异步操作.以后的异步函数,处

  • 深入分析C#中的异步和多线程

    许多开发人员对异步代码和多线程以及它们的工作原理和使用方法都有错误的认识.在这里,你将了解这两个概念之间的区别,并使用c#实现它们. 我:"服务员,这是我第一次来这家餐厅.通常需要4个小时才能拿到食物吗?" 服务员:"哦,是的,先生.这家餐厅的厨房里只有一个厨师." 我:"--只有一个厨师吗?" 服务员:"是的,先生,我们有好几个厨师,但每次只有一个在厨房工作." 我:"所以其他10个穿着厨师服站在厨房里的人--什么

  • C#异步方法返回void与Task的区别详解

    C#异步方法返回void和Task的区别 如果异步(async关键字)方法有返回值,返回类型为T时,返回类型必然是 Task<T>. 但是如果没有返回值,异步方法的返回类型有2种,一个是返回 Task, 一个是返回 void: public async Task CountDownAsync(int count) { for (int i = count; i >= 0; i--) { await Task.Delay(1000); } } public async void Count

  • c# 几个常见的TAP异步操作

    在本系列上一篇文章 [15:异步编程基础] 中,我们讲到,现代应用程序广泛使用的是基于任务的异步编程模式(TAP),历史的 EAP 和 AMP 模式已经过时不推荐使用.今天继续总结一下 TAP 的异步操作,比如取消任务.报告进度.Task.Yield().ConfigureAwait() 和并行操作等. 虽然实际 TAP 编程中很少使用到任务的状态,但它是很多 TAP 操作机理的基础,所以下面先从任务状态讲起. 1 任务状态 Task 类为异步操作提供了一个生命周期,这个周期由 TaskStat

  • C#用委托BeginInvoke做异步线程

    一个应用场景,浏览器上传一个文件,此文件后台调用文件转换,需要耗费相当长的时间,这样,如果是一个线程同步式的做下去,那么用户在浏览器上感觉就是卡住了,卡卡卡卡,这里我们利用委托的BeginInvoke和EndInvoke方法操作线程,BeginInvoke方法可以使用线程异步地执行委托所指向的方法.然后通过EndInvoke方法获得方法的返回值(EndInvoke方法的返回值就是被调用方法的返回值),或是确定方法已经被成功调用,说白了就是相当于开个多线程,你用户文件保存了之后,响应返回,这个Be

  • 一篇文章说通C#中的异步迭代器

    今天来写写C#中的异步迭代器 - 机制.概念和一些好用的特性 迭代器的概念 迭代器的概念在C#中出现的比较早,很多人可能已经比较熟悉了. 通常迭代器会用在一些特定的场景中. 举个例子:有一个foreach循环: foreach (var item in Sources) { Console.WriteLine(item); } 这个循环实现了一个简单的功能:把Sources中的每一项在控制台中打印出来. 有时候,Sources可能会是一组完全缓存的数据,例如:List<string>: IEn

  • C#异步编程几点需要注意的地方

    尽量不要编写返回值类型为void的异步方法 在通常情况下,建议大家不要编写那种返回值类型为void的异步方法,因为这样做会破坏该方法的启动者与方法本身之间的约定,这套约定本来可以确保主调方能够捕获到异步方法所发生的异常. 正常的异步方法是通过它返回的Task对象来汇报异常的.如果执行过程中发生了异常,那么Task对象就进入了faulted(故障)状态.主调方在对异步方法所返回的Task对象做await操作时,该对象若已处在faulted状态,系统则会将执行异步方法的过程中所发生的异常抛出,反之,

  • 如何在C#中使用 CancellationToken 处理异步任务

    在 .NET Core 中使用异步编程已经很普遍了, 你在项目中随处可见 async 和 await,它简化了异步操作,允许开发人员,使用同步的方式编写异步代码,你会发现在大部分的异步方法中,都提供了CancellationToken参数,本文主要介绍下 CancellationTokenSource 和 CancellationToken在异步任务中的使用. 手动取消任务 创建一个 CancellationTokenSource,然后调用异步方法时,传入 CancellationToken,它

  • c# 在Emit代码中如何await一个异步方法

    0. 前言 首先立马解释一波为啥会有这样一篇伪标题的Demo随笔呢? 不是本人有知识误区,或者要误人子弟 因为大家都知道emit写出来的都是同步方法,不可能await,至少现在这么多年来没有提供对应的功能 这是之前某天在微信群看见讨论怎么emit一个异步方法并包装异步结构,简单几句文字也未能清晰的表达 所以趁着元旦节放假有点时间, 简单列举三种我知道方式去达到这样的效果 三种方法都是绕过emit直接书写emit代码,而是将对应逻辑转到其他方法中,最后emit调用方法达到效果 Demo 说明 原始

  • c# 异步编程入门

    一.什么算异步?   广义来讲,两个工作流能同时进行就算异步,例如,CPU与外设之间的工作流就是异步的.在面向服务的系统中,各个子系统之间通信一般都是异步的,例如,订单系统与支付系统之间的通信是异步的,又如,在现实生活中,你去馆子吃饭,工作流是这样的,点菜->下单->做你的事->上菜->吃饭,这个也是异步的,具体来讲你和厨师之间是异步的,异步是如此重要,因外它代表者高效率(两者或两者以上的工作可以同时进行),但复杂,同步的世界简单,但效率极极低. 二.在编程中的异步   在编程中,

  • c# 编写一个轻量级的异步写日志的实用工具类(LogAsyncWriter)

    一说到写日志,大家可能推荐一堆的开源日志框架,如:Log4Net.NLog,这些日志框架确实也不错,比较强大也比较灵活,但也正因为又强大又灵活,导致我们使用他们时需要引用一些DLL,同时还要学习各种用法及配置文件,这对于有些小工具.小程序.小网站来说,有点"杀鸡焉俺用牛刀"的感觉,而且如果对这些日志框架不了解,可能输出来的日志性能或效果未毕是与自己所想的,鉴于这几个原因,我自己重复造轮子,编写了一个轻量级的异步写日志的实用工具类(LogAsyncWriter),这个类还是比较简单的,实

  • c# 基于任务的异步编程模式(TAP)的异常处理

    在前面讲到了<基于任务的异步编程模式(TAP)>,但是如果调用异步方法,没有等待,那么调用异步方法的线程中使用传统的try/catch块是不能捕获到异步方法中的异常.因为在异步方法执行出现异常之前,已经执行完毕. 1.没有等待的调用异步方法 ThrowAfter方法是在一定延迟后抛出一个异常: private async Task ThrowAfter(int ms,string message) { await Task.Delay(ms); Console.WriteLine("

  • c# 基于任务的异步编程模式(TAP)

    异步编程是C#5.0的一个重要改进,提供两个关键字:async和await.使用异步编程,方法的调用是在后台运行(通常在线程或任务的帮助下),但不会阻塞调用线程.异步模式分为3种:异步模式.基于事件的异步模式和基于任务的异步模式(TAP).TAP是利用关键字async和await实现的,本文将讲解TAP模式.async和await关键字只是编译器的功能.编译器最终会用Task类创建代码. 1.创建任务 建立一个同步方法Greeting,该方法在等待一段时间后,返回一个字符串. private s

随机推荐