并发模型html
常见的并发模型通常包括3类,基于线程与锁的内存共享模型,actor模型和CSP模型,其中尤以线程与锁的共享内存模型最为常见。因为go语言的兴起,CSP模型也愈来愈受关注。基于锁的共享内存模型与后二者的主要区别在于,究竟是经过共享内存来通讯,仍是经过通讯来实现访问共享内存。因为actor模型和CSP模型,本人并非特别了解,我主要说说最基本的并发模型,基于线程与锁的内存共享模型。linux
为何要并发,本质都是为了充分利用多核CPU资源,提升性能。但并发又不能乱,为了保证正确性,须要经过共享内存来协调并发,确保程序正确运转。不管是多进程并发,仍是多线程并发,要么经过线程间互斥同步(spinlock,rwlock,mutex,condition,信号量),要么经过进程间通讯(共享内存,管道,信号量,套接字),本质都是为了协同。多线程和多进程本质相似,尤为是linux环境下的pthread库,本质是用轻量级进程实现线程。下面以网络服务为例,简单讨论下多线程模型的演进。算法
最简单的模型是单进程单线程模型,来一个请求处理一个请求,这样效率很低,也没法充分利用系统资源。那么能够简单的引入多线程,其中抽出一个线程监听,每来一个请求就建立一个工做线程服务,多个请求多个线程,这就是多线程并发模型。这种模式下,资源利用率是上去了,可是却有不少浪费,线程数与请求数成正比,意味着频繁的建立/销毁线程开销,频繁的上下文切换开销,这些都是经过系统调用完成,须要应用态到内核态的切换,致使sys-cpu偏高,资源并无充分利用在处理请求上。编程
为了缓解这个问题,引入线程池模型,简单来讲,就是预先建立好一批线程,而且加大线程的复用能力,将线程数控制在必定数目内,缓解上下文切换开销。以MySQL线程池为例,原来多线程模型是单链接单线程,如今变成单语句单线程,提升了线程复用效率。若是线程在执行过程当中遇到等待(锁等待,IO等待),那么线程挂起,并减小活跃线程数,告知线程池系统活跃线程可能不够,须要追加线程,而后等系统空闲时,再减小线程数目,作到根据系统负载平衡线程数目。为了作到极致,更进一步减小上下文切换开销,引入了协程,协程只是一种用户态的轻量线程,它运行在用户空间,不受系统调度。它有本身的调度算法。在上下文切换的时候,协程在用户空间切换,而不是陷入内核作线程的切换,减小了开销。协程的并发,是单线程内控制权的轮转,相比抢占式调度,协程是主动让权,实现协做。协程的优点在于,相比回调的方式,写的异步代码可读性更强。缺点在于,由于是用户级线程,利用不了多核机器的并发执行。简单总结下:设计模式
单线程-->(单线程轮询处理,太慢)
多线程-->(多线程会频繁地建立、销毁线程,这对系统也是个不小的开销。这个问题能够用线程池来解决。)
线程池-->(仍然有多线程上下文切换的问题,调度由内核调度)
协程-->(应用层调度,不touch内核)服务器
I/O模型
linux中全部物理设备对于系统而言均可以抽象成文件,包括网卡,对应的就是套接字,磁盘对应的文件,以及管道等。所以全部对物理设备的读写操做均可以抽象为IO操做,典型的IO操做模型分为如下几类,阻塞IO,非阻塞IO,I/O多路复用,异步非阻塞IO以及异步IO等。网络
IO模型分类
阻塞I/O--> 原生的read/write系统调用,默认致使线程阻塞;
非阻塞I/O -->经过指定系统调用read/write的参数为非阻塞,告知内核fd没就绪时,不阻塞线程,而是返回一个错误码,应用死循环轮询,直到fd就绪;
I/O多路复用-->(select/poll/epoll),对通知事件堵塞,对于I/O调用不堵塞。
异步I/O(异步非阻塞)-->告知内核某个操做(读写I/O),并让内核在整个操做(包括将数据复制到咱们的进程缓冲区)完成后通知。多线程
I/O多路复用
常见的I/O多路复用主要用于网络IO场景,主要有select,poll和epoll机制。对比同步I/O,其实是对I/O请求加了一层代理,由这些代理去监听通知事件(是否网络包到来),而后再通知用户去读写数据。这种方式也是一种阻塞I/O,代理对通知事件阻塞,这里的代理通常指监听线程。对比select,poll提高了最大支持文件描述符数目,从1024提高到65535,MySQL中的半同步复制还由于使用select的这个限制,致使半同步中断的bug(连接)。并发
对比select和poll机制,epoll经过事件表管理用户感兴趣的事件,无需反复传入用户感兴趣事件,处理事件通知的时间复杂度是O(1),而select,poll机制的时间复杂度是O(N)。另外select/poll只能工做在LT模式(水平触发模式);而epoll不只支持LT模式,还支持ET模式(边缘触发模式)。两种模式的主要区别是,有数据可读时,LT模式会不停的通知,直到数据被获取,这种模式不用担忧通知事件丢失;ET模式只会通知一次,所以对比LT少不少epoll系统调用,效率更高。epoll对编程要求高,须要细致的处理每一个请求,不然容易发生丢失事件的状况。从本质上讲,与LT相比,ET模型是经过减小系统调用来达到提升并行效率的。框架
libev/libeasy
epoll很好用,可是要使用epoll,fd,signal,timer分别要采用不一样的机制才能一块儿工做。libev第一个要作的事情就是把系统资源统一成一种调用方式。由于都须要在读写事件就绪后本身负责进行读写,也就是读写过程是阻塞的。libev的核心是事件处理框架,最多见的是就是一个所谓的Reactor事件处理框架和设计模式。Reactor对象负责实现主循环(其中有事件分离器的调用),定义事件处理接口,用户程序向Reactor注册事件回调的实现类(从接口继承),Reactor主循环在收到事件的时候调用相应的回调函数。libeasy实现相似libev和libevent的功能,包括HTTP服务器等,不一样的是,它基于libev作了包装,提供了同一个的资源fd和loop机制,线程池,异步框架等实现。
AIO
说到AIO,通常是说磁盘的异步I/O,linux早期的版本并无真正的AIO接口,所谓的AIO实际上是多线程模拟的,在应用态完成。具体而言就是有一个队列存储IO请求,经过一组工做线程提取任务,并发起同步IO,待IO完成后,再通知用户已经完成了。对于用户而言,因为是提交IO请求后就直接返回,而后再被通知IO已经完成,因此能够认为是异步I/O,这种异步I/O实现机制主要指POXIS AIO,MySQL的InnoDB引擎也实现了一套相似的AIO机制。后面linux内核引入了真正的AIO,主要区别在于发起I/O调用再也不是同步调用,IO请求统一在内核层面排队,而且一次能够提交一批异步IO请求,而后经过轮询或者回调的方式接收完成通知便可。相比于POXIS AIO,底层有更多的IO并行,IO和CPU能充分并发,大大提高性能。在使用中,经过-lrt连接使用AIO库是POXIS接口,而经过-laio连接使用的AIO库是linux Native AIO接口。经常使用接口包括 io_setup,io_destroy,io_submit,io_cacel和io_getevents等。
同步IO:
优势:简单
缺点:IO阻塞,没法充分利用IO和CPU资源,效率低
Native AIO:
优势:AIO能够支持一次发送多个不连续的异步IO请求,性能更好(同步IO须要发送屡次)
缺陷:须要文件系统支持O_DIRECT选项,若是不支持,io_submit其实是“退化”成同步操做。
POSIX AIO:
优势:不依赖O_DIRECT选项,有必定的合并能力(相邻地址的请求,能够作merge)。
缺点:并发的IO请求受限于线程数目;另外就是,可能慢速磁盘,可能致使其它新的请求没有及时处理(工做线程数不够了)。
参考文档
https://cloud.tencent.com/developer/article/1349213
https://my.oschina.net/dclink/blog/287198
http://www.javashuo.com/article/p-wsiwtuxy-m.html
https://www.ibm.com/developerworks/cn/linux/l-async/