C# 异步编程笔记(上)

在了解异步编程之前,不妨先温习一下它和它的孪生兄弟(多线程)之前的区别和联系。

多线程:

线程是CPU的调度单位,对于CPU来说,它面对的就是一个个线程。根据操作系统的调度算法,CPU“时间片“被轮流分配给各个线程,线程得以在CPU上执行。当然,一个CPU在某个确切的时间只能运行一个线程。而现代计算机大部分都是多个CPU,所以如果一个进程只有一个线程(而且还常常要挂起不占CPU资源),可以想到CPU的大部分资源都在空闲状态。

所以,最初设计多线程的最核心的目的是为了提高资源利用率,让CPU保持”忙碌“,从而提高系统吞吐量。如果我计算机有四个CPU,而且应用程序有四个线程,那么四个CPU都会很忙。其结果就是,只需要一个任务的时间,四个任务都可以完成。

当然,多线程的作用也不仅仅是提高CPU利用率。还有诸多用处:比如提高软件的可响应性,以及一些适合在后台偷偷做的事情,或者是支持一些设计模式,比如一个较大型程序把几个部分独立的放在不同线程中,以便保持各个part的独立性,某个part出故障了,其它的仍然可以运行,这样的程序也更易于扩展。

异步:

异步的概念很简单:我让别人帮我拿个东西,我不会什么都不做的傻等他(先去处理别的代码,等这件事完成了再回来处理它),所以异步是针对当前线程 处理 代码 时序上的概念

主线程调用了一个非常耗时的操作,不会傻等它完成,而是先继续做接下来的事,这就叫异步。什么都不做,等到这个操作完成再继续做接下来的事就是同步。
但是不管同步还是异步,这个耗时的操作总是要做,这部分代码总是要执行。同步就是在同一线程中执行(程序按顺序执行)。异步就是让它在另外一个线程中执行(两部分代码并发执行),从而不阻塞父线程

可以说这么说:异步总是离不开多线程

总的来说:
1、异步和多线程两者都可以达到避免调用线程阻塞的目的,从而提高软件的可响应性。
2、多线程是异步的一种实现手段,反过来,也可以说异步是多线程的一个封装分支。


异步I/O:

异步I/O 是异步编程的重中之重。如果没有异步I/O,我想大家也懒得区别多线程和异步之间的区别。
异步I/O之所以“重要”,得益于有专门的硬件支持I/O操作,以及Windows的IOCP(I/O完成端口)。使得异步I/O不需要额外的线程,并且大大的提高了高并发下的异步I/O的效率。

与计算机执行的大多数操作相比,设备I/O是其中最慢,最不可预测的操作之一。CPU从文件或者跨网络读写数据的速度,比它执行的算术运算速度慢的多的多。
容易想到的一个解决办法是开启一个新的线程,在这个线程中专门处理I/O操作。从来不会影响到父线程的继续运行,这也是之前说道的多线程的一个作用提高软件响应性。
如果确实没有更好的解决办法的话,这这样做也却是不错。但是实际上有,那就是”异步I/O“异步I/O允许将任务交给硬件设备来处理,期间完全不占用线程和CPU资源。(请一定区分”异步I/O“和开一个线程做I/O的区别!)

假设一个线程向设备发出一个异步I/O请求。这个I/O请求被传给设备驱动程序,后者负责完成实际的I/O操作。当驱动程序在等待设备响应的时候,应用程序的线程并没有因为要等待I/O请求而挂起,线程会继续运行并执行其他有用的任务。到了某一时刻,设备驱动程序完成了队列中的I/O操作,再通知线程操作已完成。

再说一遍,异步I/O操作进行期间,完全不占用线程和CPU资源There Is No Thread

FileStream stream = new FileStream("in.txt", FileMode.OpenOrCreate,
                            FileAccess.ReadWrite, FileShare.ReadWrite, 1024, FileOptions.Asynchronous);
byte[] byteData = ...;

// 方法一                          
Thread thread = new Thread(new ThreadStart(ReadFile));
thread.Start();

static void ReadFile()
{
    ...
    stream.Read(byteData, 0, byteData.Length);
}

// 方法二
stream.ReadAsync(byteData, 0, byteData.Length);

如上伪代码,方法一手动创建一个线程,在里面执行I/O操作,和方法二直接调用异步I/O函数的代价就有了显著的差别!前者要创建、销毁线程,占用大量的CPU和内存资源,这些对于方法二来说都是完全浪费的,多余的,不必要的操作。

还有一些程序员喜欢向下面这样写(我亲眼看到过一个资深程序员这样写)

//方法三
Task.Run(() =>{
    stream.ReadAsync(byteData, 0, byteData.Length);
});

大哥,完全是在画蛇添足

I/O操作执行完之后,计算机必须要通知发出请求的程序,I/O操作已完成,请处理。
所以,对于不是同步的代码来说,它必须留下一部分资源来监听处理这个”I/O已完成消息“。

Windows有一个非常牛逼的内核对象IOCP(I/O competition port),专门用来处理”I/O已完成消息“。
其效率之高完全碾压其它之前的方法。
方法二ReadAsync 正是使用了IOCP。若是用方法一这种写法,也享受不到完成端口的好处。

完成端口不是两句话能解释清楚的,但它确实非常重要,但是由于和线程池联系很深,所以我把它放到下篇和线程池一起说。

如你所想,C#(.NET)基本为所有的I/O操作都提供了专门的异步函数。上面伪代码中的ReadAsync 正是其中之一。

异步I/O在异步界的地位非常重要,甚至有些人直接把“异步I/O”等同于“异步”,那么除了I/O操作之外,就没有其它费时的操作了吗?当然有,比如图像处理等虽然不是I/O但是也需要大量的算术运算。非常耗时。

但是,对于图像处理等操作来说,没有“专门的硬件”来支持它去不占CPU的运算,当然也就不存在专门针对它的异步操作函数。所以,为了不阻塞主线程,这时候只能new 一个Thread在后台运行了。

而此时,这到底是叫多线程,还是叫异步,没人关心也无需关心了。


总结

异步I/O之所以厉害,依靠的有两点:
1、 专门的硬件支持,如DMA(直接内存访问),可以让I/O设备不通过CPU直接和内存交流。
2、 IOCP,性能极高的异步I/O处理内核对象。

另外,多线程和异步之间有着斩不断的联系,即使是异步I/O操作不需要线程,但是异步I/O操作的结果处理,却都是在另外一个线程中。

之所以有上篇,是尝试搞清楚异步I/O的”来龙去脉“,而不仅仅停留于表面,百度一下函数的用法,copy到代码中, 甚至是出现上面说的方法三的那种画蛇添足的用法。

下篇当然就是具体异步编程的用法了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值