草莓味的棉花糖|深入理解异步I/O+epoll+协程

前言同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;而异步是指用户线程发起IO请求后仍继续执行 , 当内核IO操作完成后会通知用户线程 , 或者调用用户线程注册的回调函数 。 阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:阻塞是指IO操作需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值 , 无需等到IO操作彻底完成 。
异步I/O在理解异步I/O之前 , 我们先要知道什么是同步I/O在阻塞同步I/O模型下 , 用户线程向内核发起 recvfrom 系统调用 , 当数据没有准备好的时候 , 用户线程阻塞 。 此外还有一种非阻塞同步I/O , 此时用户线程不阻塞于 recvfrom , 而是反复向系统查询数据状态 。 当数据准备好了 , 就对数据进行后续处理 。
草莓味的棉花糖|深入理解异步I/O+epoll+协程Paste_Image.png
而在异步I/O模式下 , 用户线程在数据还没有准备好的时候既不阻塞也不反复查询 , 而是继续干自己该干的事情 。 内核会开启一个内核线程去读取数据 , 等到数据准备好了 , 内核给用户线程一个信号 , 用户线程中断去执行信号处理函数 。
草莓味的棉花糖|深入理解异步I/O+epoll+协程Paste_Image.png
node.js中的异步回调也是采用开线程的方式实现的
epollepoll/select 是一种I/O多路复用模型用户线程可以先通过 epoll 注册多个I/O事件然后用户线程反复执行调用 epoll/select 查询是否有准备好的事件如果有准备好的I/O事件则进行处理关键点是一个用户线程处理多个I/O事件
epoll/select 的区别在于select 的底层原理是遍历所有注册的I/O事件 , 找出准备好的的I/O事件 。 而 epoll 则是由内核主动通知哪些I/O事件需要处理 , 不需要用户线程主动去反复查询 , 因此大大提高了事件处理的效率 。
草莓味的棉花糖|深入理解异步I/O+epoll+协程Paste_Image.png
协程协程是一种轻量级的线程本质上协程就是用户空间下的线程如果把线程/进程当作虚拟“CPU” , 协程即跑在这个“CPU”上的线程 。
协程的特点

  1. 占用的资源更少 。
  2. 所有的切换和调度都发生在用户态 。
不管是进程还是线程 , 每次阻塞、切换都需要陷入系统调用 , 先让CPU跑操作系统的调度程序 , 然后再由调度程序决定该跑哪一个线程 。 而且由于抢占式调度执行顺序无法确定的特点 , 使用线程时需要非常小心地处理同步问题 , 而协程完全不存在这个问题 。 因为协程可以在用户态显示控制切换
【草莓味的棉花糖|深入理解异步I/O+epoll+协程】例子传统的生产者-消费者模型是一个线程写消息 , 一个线程取消息 , 通过锁机制控制队列和等待 , 但一不小心就可能死锁 。
如果改用协程 , 生产者生产消息后 , 直接通过yield跳转到消费者开始执行 , 待消费者执行完毕后 , 切换回生产者继续生产 , 效率极高:
import timedef consumer():r = ''while True:n = yield rif not n:returnprint('[CONSUMER] Consuming %s...' % n)time.sleep(1)r = '200 OK'def produce(c):c.next()n = 0while n < 5:n = n + 1print('[PRODUCER] Producing %s...' % n)r = c.send(n)print('[PRODUCER] Consumer return: %s' % r)c.close()if __name__=='__main__':c = consumer()produce(c)


推荐阅读