当前位置:   article > 正文

.NET多线程和异步总结(一)

.net 多线程和异步

前言

本文源于笔者在公司内部的一个分享。几月前为了搞懂这些知识花费了大量的时间调查研究,最终的理解算是全面而透彻了。而现在学习其他技术时,间或会遇到与此类似的话题,于是把先前的总结记录下来,以作备忘,并启发自己触类旁通。文中图片都取自当时的Slides。

为何要关注多线程和异步

服务器的计算分为IO计算和CPU计算。IO计算指计算任务中以IO为主的计算模型,比如文件服务器、邮件服务器等,混合了大量的网络IO和文件IO;CPU计算指计算任务中没有或很少有IO,比如加密/解密,编码/解码,数学计算等等。

需要关心的是IO计算,一般的网络服务器程序往往伴随着大量的IO计算。提高性能的途径在于要避免等待IO 的结束,造成CPU空闲,要尽量利用硬件能力,让一个或多个IO设备与CPU并发执行。

另一方面,CPU密集的计算是我们无法控制的。如果是CPU计算出现了瓶颈,那只能给服务器增加CPU,或者增加服务器。而IO操作,实际上是空等别的硬件,这里面的优化就大有可为。

大部分Web服务的大部分操作都是IO密集型的。无非是读磁盘、查数据库、访问网络调用别的API,而这些都是通过操作系统的IO。
多线程是必须的,但线程并非越多越好

为了降低请求等待时间,对每个请求都建立一个线程来处理可以吗?这是很多早期服务器的方式。而这种方式存在很大的问题。

首先,创建线程/进程和销毁线程/进程的代价非常高昂,尤其是在服务器采用TCP“短连接”方式或UDP方式通讯的情况下,例如,HTTP协议中,客户端发起一个连接后,发送一个请求,服务器回应了这个请求后,连接也就被关闭了。如果采用经典方式设计HTTP服务器,那么过于频繁地创建线程/销毁线程对性能造成的影响是很恶劣的。

线程还会占用内存。线程的内核对象占几M到几时M。普通的计算机上千个线程就会耗尽内存。

线程切换也会消耗时间,最终导致服务器性能急剧下降。如果客户端并发请求量很高,同一时刻有很多客户端等待服务器响应的情况下,将会有过多的线程并发执行,频繁的线程切换将用掉一部分计算能力。

对于一个需要应付同时有大量客户端并发请求的网络服务器来说,线程池是唯一的解决方案。线程池不光能够避免频繁地创建线程和销毁线程,而且能够用数目很少的线程就可以处理大量客户端并发请求。

异步IO是关键

只有线程池,而继续同步等待IO请求等于没用,可达到的吞吐量将非常有限。异步IO则可以让线程池中的每个线程用很短的时间处理请求中的计算任务,然后把任务交给IO,线程继续处理别的请求。当IO返回时,一个线程池中的线程再被指派来完成剩下的工作,直到完成请求的响应。如此一来,就可以压榨出最大的硬件性能。
这也是为何Nodejs将异步作为核心卖点,各大主流语言和框架都有类似的异步支持和async/await句式了。

从IO讲起

Windows IO的基本过程如下:

clipboard.png

其中要用到中断和APC,简要介绍一下:

中断

CPU在执行指令的间隙可以被中断打断,进入内核态。内核态共享一个地址空间,没有进程线程之分。CPU将执行中断分发例程,保存执行现场,执行中断服务例程,再恢复现场,继续执行原来的的指令。

clipboard.png

中断还有优先级的概念。处于高优先级时,低优先级的中断不能被处理,直到CPU中断优先级降下来。

clipboard.png

APC

是操作系统提供的一种异步回调机制。可以把一段任务代码放到某个用户线程的内核结构的某个队列中,程序正常运行时不会执行。只有当线程发起某些调用,使自己成为Alertable(可唤醒)时,才会检查APC队列,把其中的任务都执行了。这些进入可唤醒状态的调用有:

  • WaitForSingleObjectEx()
  • SleepEx() 等等。

再来看第一幅图。通过SYSCALL指令进行中断调用,进入内核态。内核调用驱动程序去完成IO。而调用是否立即返回(异步),还是阻塞等待(同步),取决于传入的参数。另一张.NET IO的图则可以提供更多理解。

clipboard.png

Windows IO的几种模式

  1. 同步模式

    • 调用 CreateFile(), ReadFile(), WriteFile()等API的默认方式就是同步阻塞。
  2. 异步模式

    • Overlapped结构体

      • CreateFile(..., FILE_FLAG_OVERLAPPED,...) 打开文件时加入此标志启动异步模式,API调用会立即返回。
      • 使用GetOverlappedResult/WaitForMultipleObjects等待Overlapped结构体,可以等待IO完成。
    • APC完成例程

      • 如此调用WriteFile(..., lpCompletionRoutine),传入一个IO完成例程,内核会在IO完成时将此例程放入线程APC队列。此线程再在适时调用SleepEx()等进入可唤醒状态,执行IO完成例程。
    • IOCP(IO 完成端口)

      • CreateIoCompletionPort()创建IO完成端口。
      • 调用GetQueuedCompletionStatus()等待IO完成端口上面的IO完成信号。IO完成端口会保证只触发合适数量的线程(约等于CPU核心数)来处理IO完成后的工作。
      • 调用PostQueuedCompletionStatus()来关闭IO完成端口。

以上讲解对于不了解Windows核心编程的读者来说肯定无法形成什么认识。以下通过一个例子来介绍这几种模式。

一个Socket服务器的示例

这里用一个Winsock2(Socket的Windows版)的程序举例。只作图示,具体代码未必完全契合。
红色表示阻塞。

  1. 同步模型。单线程,一次只能处理一个请求。

    clipboard.png

  2. 多线程。每次请求都另起线程处理,能同时处理多个请求了。但是线程也越变越多,抗不住。

    clipboard.png

  3. 使用Overlapped结构体实现异步。只有一个线程,调用GetOverlappedResult()等待多个Overlapped结构,有完成的IO就解决处理,然后继续等待。一个线程就可以处理所有请求!

    clipboard.png
    可惜它也有缺点。等待多个对象相对于WaitForMultipleObjects(),而这个函数有64个等待对象的上限。所以只能用多线程来等待。当请求量很大时,线程数量也会变大。

  4. 使用APC。同样是一个线程,在调用IO函数时传入回调,然后不停地进入可唤醒态,等待IO完成后触发APC回调。

    clipboard.png
    缺点:线程执行的主动性不在自己;负载均衡不易控制。如今已极少使用。

  5. 使用IOCP。Windows的大杀器。IOCP可以激活合适数量的线程来执行IO完成后的任务。

    clipboard.png
    可以把IOCP理解成一种内核同步机制。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/84356
推荐阅读
相关标签
  

闽ICP备14008679号