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

前言

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

  对于c#,异步处理经过了多个阶段,但是对于现阶段异步就是Task,微软用Task来抽象异步操作。以后的异步函数,处理的都是Task。你会看到处处都是task的身影。为了处理Task,c#引入了两个关键词async,await。这两个关键词也可以说是一个关键词,因为async的存在是为了表明await是关键词。总而言之:两个关键词干了一件事,async关键词并不改变函数的声明。

  有人说await就是语法糖,不值得大书特书,我只能说你错了。软件开发坚持的原则为:代码要省,代码要清晰易懂!如果没有语法糖,代码的维护性大大降低。await这个语法糖做的事很多;如果不用await,处理同样的逻辑,需要多写很多代码,并导致逻辑不清晰。

Task的分类

  异步分为两类 compute-base 和 IO-base。compute-base就是计算密集型,函数所有的操作都是在内存中,不涉及IO;如果运行这个函数,则单个线程利用率达100%;IO-base就是涉及到IO,IO包括文件读写,socket读写;这类异步操作底层涉及到IOCP(完成端口)。相应的,Task也分为两类。

  对于这两个区别可以举个例子来区分:一台电脑为4个线程。如果同时有4个compute-base线程运行,cpu的利用率为100%。如果同时有4个 IO-base的异步操作,cpu利用率可能远远低于100%。

  对于.net 库,有些函数会有两个版本:一个是同步操作,一个是异步操作(函数名以Async结尾,返回值为Task)。举个例子:

这是WebClient类获取网址内容函数。你会问DownloadStringTaskAsync是compute-base  Task,还是 IO-base Task?我可以肯定的告诉你:只要是.net基本类库提供的异步函数基本都是IO-base Task(微软官方文档是这样要求)。其实这样要求是有道理的:对于compute-base异步,比较容易封装;再者,这样的异步是不能大规模的并发的。如果16个线程cpu,同时并发16个这样的异步操作就是上限了;如果再多,反而会有害!

  有人说,如果基本类库不提供 IO-base Task函数,我也可以封装一下,这个也不难啊!代码如下:

//把一个同步操作,改造成异步
public static async Task<byte[]> DownloadDataAsync(string url)
{
   WebRequest request = WebRequest.Create(url);

   return await Task.Run(() =>
   {
    using (var response = request.GetResponse())
    using (var responseStream = response.GetResponseStream())
    using (var result = new MemoryStream())
    {
     responseStream.CopyTo(result);
     return result.ToArray();
    }
   });
 }

  上面函数如果说是异步操作,也不错。但是,这不是“好”的异步操作!这是异步操作中夹杂着同步IO。会导致线程等待。如果有100个这样的异步操作,就需要100个线程,这些线程大部分并没在干活,而是在等待! 对于“好”的异步IO,如果同时有100个操作,甚至几万个操作,使用的线程都是有限的,一般不超过cpu线程数。这是怎么实现的?这涉及到IOCP,说起来有些复杂,可以参考IOCP相关资料。类库提供异步IO操作,都是涉及到IOCP的。所以得到如下结论: 如果类库不提供IO异步函数,无论怎么改造,不可能改造成“好”的异步函数!

Task实现的基本原理

Task变量状态如下

  状态简要分为生成、执行、执行完毕这三个阶段。如果执行完毕前获取执行后的值Task.Result,函数就会阻塞。那我怎么知道什么时候完成,而又不阻塞?有两种办法,轮询和回调通知。Task.IsCompleted属性会指示函数是否执行完毕。轮询不是一个好的办法,采用回调通知是上策!

  回调通知有个缺点:处理逻辑不直观,回调函数与异步调用函数不在一块,还有可能隔着很多行代码或不在同一个文件。如果这样的回调函数太多,对理解代码逻辑造成困难,代码不易维护。微软也考虑到了这个问题,那就用await关键词来解决。await帮你处理了回调函数的弊端,其实await后面的代码与await前面的代码不属于同一个函数!await后面的代码就是回调函数!微软确实给我们解决了这个问题,但是又带来另一个问题。好多人不明白,明明是同一个函数,怎么实现了等待而又不阻塞当前线程!归根到底,还是要理解await背后帮你干了啥,否则就会一直困惑。

  要生成Task变量,只要理解几个关键的处理步骤就行了。TaskCompletionSource类会帮助我们生成Task。如果IO完成,设置Task的状态为完成就行了。后面,就会执行回调函数(await关键词帮我干了,你看不到回调)!

如何写一个IO-base Task函数?

  大部分情况下不需要自己写这样的函数。但是,人是有好奇心的,如果不明白函数实现的原理,总是感觉不能释怀!再者,明白函数实现原理,就能更好的利用这类函数。下面讲解一下如何利用IOCP来实现异步函数。我没有参考.net的源码,只是根据逻辑推理应该这实现。肯定和.net源码实现有出入,我写这些代码主要为了阐明Task实现原理。

IOCP处理逻辑

  对于IOCP,这里不展开来讲了,否则就跑题了。以socket读取为例子,简单总结一下:如果你要接收100个字节的数据,你告诉IOCP你要接收100个字节数据,并提供100个字节的buffer,函数立即返回;数据到达后,IOCP通知你,数据到了,数据就存在你提供的buffer里。

  实现异步IO伪代码如下:

class AyncInside
 {
  //完成端口句柄
  IntPtr iocpHandle = IntPtr.Zero;

  Task<byte[]> ReadFromSocket(int count)
  {
   //生成此次操作需要相关数据
   TaskCompletionSourceRead readInfo = new TaskCompletionSourceRead();
   readInfo.Buffer = new byte[count];

   //如果没生成iocp则生成。
   if (iocpHandle == IntPtr.Zero)
   {
    iocpHandle = CreateIocp();
   }

   // 告诉iocp,要读取count字节数据。函数不会阻塞,会立即返回
   //从完成端口收到数据后,会调用ReadScoketCallback
   //我们把readInfo也传给函数。当回调时,该变量会传给回调函数。
   ReadFromIocp(iocpHandle, readInfo.Buffer, readInfo, ReadScoketCallback);

   return readInfo.Tcs.Task;
  }

  void ReadScoketCallback(byte[] buffer, int readCount,object tag)
  {
   //tag就是调用ReadFromIocp时,传的readInfo
   //便于我们知道异步调用时的上下文数据。
   TaskCompletionSourceRead readInfo = tag as TaskCompletionSourceRead;

   if(buffer.Length == readCount )
   {
    //调用完SetResult后,await后面的代码就会被执行!
    readInfo.Tcs.SetResult(buffer);
   }
   else if (buffer.Length > 0)
   {
    Array.Resize(ref buffer, readCount);
    readInfo.Tcs.SetResult(buffer);
   }
   else
   {
    readInfo.Tcs.TrySetException(new Exception("读取数据异常!socket可能已断开!"));
   }
  }

  private void ReadFromIocp(IntPtr iocpHandle, byte[] buffer, object tag,
   Action<byte[] , int,object> readScoketCallback)
  {
   throw new NotImplementedException();
  }

  private IntPtr CreateIocp()
  {
   throw new NotImplementedException();
  }

 }

 //封装异步读取需要的数据
 class TaskCompletionSourceRead
 {
  public TaskCompletionSource<byte[]> Tcs { get; set; }
  public byte[] Buffer { get; set; }
 }

  上述代码与实际可使用代码差距还很大,我在这里主要为了阐明原理。通过上面的代码,我们可以看到,这个异步函数并没生成新的线程;网卡驱动和IOCP配合,帮我们接收了数据。所以这种方式才是真正可扩展的异步IO。

后记

异步IO和可扩展服务紧密关联。对于.net core平台,你会看到很多函数都是异步的。理解和用好异步IO函数非常重要。本文通过自己对异步IO的理解,试图通过代码阐明异步IO实现原理。希望你看过此文后,能对此有更深的理解!如果此文对你有所裨益,希望您给点个赞!

以上就是基于c# Task自己动手写个异步IO函数的详细内容,更多关于c# Task手写异步IO函数的资料请关注我们其它相关文章!

(0)

相关推荐

  • 深入分析C# Task

    ​Task的MSDN的描述如下: [Task类的表示单个操作不会返回一个值,通常以异步方式执行. Task对象是一种的中心思想基于任务的异步模式首次引入.NETFramework 4 中. 因为由执行工作Task对象通常以异步方式执行线程池线程上而不是以同步方式在主应用程序线程中,可以使用Status属性,并将IsCanceled, IsCompleted,和IsFaulted属性,以确定任务的状态. 大多数情况下,lambda 表达式用于指定该任务所执行的工作量. 对于返回值的操作,您使用Ta

  • C# 并行和多线程编程——Task进阶知识

    一.Task的嵌套 Task中还可以再嵌套Task,Thread中能不能这样做,我只能说我是没这样写过.Task中的嵌套,我感觉其实也可以分开来写,不过嵌套起来会方便管理一点.Task中的嵌套分为两种,关联嵌套和非关联嵌套,就是说内层的Task和外层的Task是否有联系,下面我们编写代码先来看一下非关联嵌套,及内层Task和外层Task没有任何关系,还是在控制台程序下面,代码如下: static void Main(string[] args) { var pTask = Task.Factor

  • c# 使用Task实现非阻塞式的I/O操作

    在前面的<基于任务的异步编程模式(TAP)>文章中讲述了.net 4.5框架下的异步操作自我实现方式,实际上,在.net 4.5中部分类已实现了异步封装.如在.net 4.5中,Stream类加入了Async方法,所以基于流的通信方式都可以实现异步操作. 1.异步读取文件数据 public static void TaskFromIOStreamAsync(string fileName) { int chunkSize = 4096; byte[] buffer = new byte[chu

  • windows下C#定时管理器框架Task.MainForm详解

    入住博客园4年多了,一直都是看别人的博客,学习别人的知识,为各个默默无私贡献自己技术总结的朋友们顶一个:这几天突然觉得是时候加入该队列中,贡献出自己微弱的力量,努力做到每个月有不同学习总结,知识学习的分享文章.以下要分享的是花了两天时间编写+测试的windows下C#定时管理器框架-Task.MainForm. 目的: 随着这五年在几个公司做不同职位的.net研发者,发现各个公司都或多或少会对接一些第三方合作的接口或者数据抓取功能,都是那种各个服务直接没有关联性功能,开发人员也可能不是一个人,使

  • C# 并行和多线程编程——认识和使用Task

    对于多线程,我们经常使用的是Thread.在我们了解Task之前,如果我们要使用多核的功能可能就会自己来开线程,然而这种线程模型在.net 4.0之后被一种称为基于"任务的编程模型"所冲击,因为task会比thread具有更小的性能开销,不过大家肯定会有疑惑,任务和线程到底有什么区别呢? 任务和线程的区别: 1.任务是架构在线程之上的,也就是说任务最终还是要抛给线程去执行. 2.任务跟线程不是一对一的关系,比如开10个任务并不是说会开10个线程,这一点任务有点类似线程池,但是任务相比线

  • C# task应用实例详解

    Task的应用 ​Task的MSDN的描述如下: [Task类的表示单个操作不会返回一个值,通常以异步方式执行. Task对象是一种的中心思想基于任务的异步模式首次引入.NETFramework 4 中. 因为由执行工作Task对象通常以异步方式执行线程池线程上而不是以同步方式在主应用程序线程中,可以使用Status属性,并将IsCanceled, IsCompleted,和IsFaulted属性,以确定任务的状态. 大多数情况下,lambda 表达式用于指定该任务所执行的工作量. 对于返回值的

  • C#利用Task实现任务超时多任务一起执行的方法

    前言 其实Task跟线程池ThreadPool的功能类似,不过写起来更为简单,直观.代码更简洁了,使用Task来进行操作.可以跟线程一样可以轻松的对执行的方法进行控制. 创建Task有两种方式,一种是使用构造函数创建,另一种是使用 Task.Factory.StartNew 进行创建. 如下代码所示 1.使用构造函数创建Task Task t1 = new Task(MyMethod); 2.使用Task.Factory.StartNew 进行创建Task Task t1 = Task.Fact

  • C#中Task.Yield的用途深入讲解

    前言 最近在阅读 .NET Threadpool starvation, and how queuing makes it worse这篇博文时发现文中代码中的一种 Task 用法之前从未见过,在网上看了一些资料后也是云里雾里不知其解,很是困扰.今天在程序员节的大好日子里终于想通了,于是写下这篇随笔分享给大家,也过过专心写博客的瘾. 这种从未见过的用法就是下面代码中的 await Task.Yield() : static async Task Process() { await Task.Yi

  • C#关于Task.Yeild()函数的讨论

    在与同事讨论async/await内部实现的时候,突然想到Task.Yeild()这个函数,为什么呢,了解一点C#async/await内部机制的都知道,在await一个异步任务(函数)的时候,它会先判断该Task是否已经完成,如果已经完成,则继续执行下去,不会返回到调用方,原因是尽量避免线程切换,因为await后面部分的代码很可能是另一个不同的线程执行,而Task.Yeild()则可以强制回到调用方,或者说主动让出执行权,给其他Task执行的机会,可以把Task理解为协程,Task.Yeild

  • c#异步task示例分享(异步操作)

    c# Task异步操作 复制代码 代码如下: using System;using System.Threading;using System.Threading.Tasks; namespace ConsoleApplication18{    class Program    {        static void Main(string[] args)        {            Func<string, string> _processTimeFunc = new Fun

  • 详解C#中 Thread,Task,Async/Await,IAsyncResult的那些事儿

    说起异步,Thread,Task,async/await,IAsyncResult 这些东西肯定是绕不开的,今天就来依次聊聊他们 1.线程(Thread) 多线程的意义在于一个应用程序中,有多个执行部分可以同时执行:对于比较耗时的操作(例如io,数据库操作),或者等待响应(如WCF通信)的操作,可以单独开启后台线程来执行,这样主线程就不会阻塞,可以继续往下执行:等到后台线程执行完毕,再通知主线程,然后做出对应操作! 在C#中开启新线程比较简单 static void Main(string[]

  • 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

随机推荐