协程

1. 协程的概念

程序之间的切换叫 协程

协程,又称微线程,纤程。英文名Coroutine。

线程系统级别的,它们由操作系统调度,而协程则是程序级别的,由程序根据需要自己手动调度

一个线程中会有很多函数,我们把这些 函数 称为 子程序在子程序执行过程中 可以中断 去执行别的子程序,而别的子程序 也可以中断回来 继续执行之前的子程序,这个过程就称为 协程

def a():
    for i in range(100):
      print(i)
  
def b():
    for i in range(100):
      print(i)
  
def work():
  a()
  b()
 
 比如说:
 上面work中,a 方法 循环到50次了,需要用到  b 方法 运行的结果才能
 继续往下执行,咱们可以根据需要,想执行哪个就执行哪个,这就是 协程

也就是说在同一线程内一段代码在执行过程中会中断然后跳转执行别的代码,接着在之前中断的地方继续开始执行,类似与 yield操作

协程 拥有自己的 寄存器上下文(其实就是起记录作用,好标记上次运行到哪里了,这次回来接着上次的运行)。
协程调度切换时,将 寄存器上下文 保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。

因此:
协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态

换种说法:进入上一次离开时所处逻辑流的位置。

协程的优点

  1. 无需线程上下文切换的开销,协程避免了无意义的调度,由此可以提高性能(但也因此,程序员必须自己承担调度的责任,同时,协程也失去了 标准线程 使用 多CPU 的能力)
  2. 无需原子操作锁定及同步的开销
  3. 方便切换控制流,简化编程模型
  4. 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

协程的缺点

  1. 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU多个核用上,协程 需要和 进程 配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是CPU密集型应用。
  多协程 在 一个CPU 里
  1. 进行阻塞(Blocking)操作(如 IO 时)会阻塞掉整个程序回到顶部

2. python3实现协程

(1)yield实现协程效果

def consumer(name):     # 消 费 者     [ 第 一 步 ]
    print('开始吃包子...')
    while True:             
       print('\033[31;1m[consumer]%s需要包子\033[0m'%name)    [ 第 二 步 ]
       bone = yield        # 接 收 send 发 送 的 数 据        [ 阻 塞 ]
       print('\033[31;1m[%s]吃了%s个包子\033[0m'%(name,bone))

def producer(obj1):     # 生 产 者
    obj1.send(None)     # 必 须 先 发 送 None
    for i in range(3):
        print('\033[32;1m[producer]\033[0m正在做%s个包子'%i)
        obj1.send(i)

if __name__ == '__main__':  #  主 程 序;程序运行的入口
   con1 = consumer('消费者A')  # 创 建 消 费 者 对 象
   producer(con1)

#output:
开始吃包子…
[consumer]消费者A需要包子
[producer]正在做0个包子
[消费者A]吃了0个包子
[consumer]消费者A需要包子
[producer]正在做1个包子
[消费者A]吃了1个包子
[consumer]消费者A需要包子
[producer]正在做2个包子
[消费者A]吃了2个包子
[consumer]消费者A需要包子
在这里插入图片描述

def a():
  print("进入a方法")
  #bone = yield
  print("结束a方法")
def b():
   for i in range(100):
     print(i)
def work():
   a()
   b()
work()

编译结果:
进入a方法
结束a方法
0 - 99

def a():
   print("进入a方法")
   bone = yield
   print("结束a方法")

def b():
   for i in range(100):
    print(i)

def work():
   a()
   b()

work()

编译结果:
0 - 99
在这里插入图片描述
在这里插入图片描述

(2)greenlet模块实现程序间切换执行

import greenlet   # 导入第三方库

# 创建三个线程

def A():
   print('a.....')     #【 第 一 步 】
   g2.switch()         # 切 换 至 B
   print('a....2')      #【 第 三 步 】
   g2.switch()

def B():
   print('b.....')     # 【 第 二 步 】
   g1.switch()         # 切 换 至 A
   g3.switch() 
   print('b....2')     # 【 第 五 步 】

def C():
   print("c......")    # 【 第 四 步 】
   g2.switch()

g1 = greenlet.greenlet(A)  # 创 建 一 个 线 程
g2 = greenlet.greenlet(B)
g3 = greenlet.greenlet(C)

g1.switch()     #  谁 点 switch 就 谁 运 行

在这里插入图片描述

(3)gevent实现协程 (这个实现协程是比较常用的,也是比较难的一个)

Gevent 是一个第三方库,可以轻松通过 gevent 实现 协程,在 gevent 中用到的主要模式是 Greenlet , 它是以C扩展模块形式接入Python的 轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

gevent 会主动识别程序内部的 IO 操作,当子程序遇到 IO 后,切换到别的 子程序。如果所有的 子程序 都进入 IO,则 阻塞

import gevent

def foo():
    print('running in foo')
    gevent.sleep(2)
    print('com back from bar in to foo')

def bar():
    print('running in bar')
    gevent.sleep(2)
    print('com back from foo in to bar')

gevent.joinall([      # 创 建 线 程 并 行 执 行 程 序 ,碰 到 I O 就 切 换
gevent.spawn(foo),
gevent.spawn(bar),
])

线程函数 同步 与 异步 比较:

import gevent

def task(pid):
    gevent.sleep(1)
    print('task %s done' %pid)

def synchronous():     # 同 步 一 个 线 程 执 行 函 数
    for i in range(1,10):
       task(i)

def asynchronous():    # 异 步 一 个 线 程 执 行 函 数
    threads = [gevent.spawn(task,i) for i in range(10)]
    gevent.joinall(threads)

print('synchronous:')    
synchronous()          # 同 步 执 行 时 要 等 待 执 行 完 后 再 执 行    
print('asynchronous:')    
asynchronous()         # 异 步 时 遇 到 等 待 则 会 切 换 执 行

爬虫异步IO 阻塞 切换:

from urllib import request
import gevent,time
from gevent import monkey

monkey.patch_all()   # 将 程 序 中 所 有 IO 操 作 做 上 标 记 使 程 序 非 阻 塞 状 态

def url_request(url):
    print('get:%s'%url)
    resp = request.urlopen(url)
    data = resp.read()
    print('%s bytes received from %s'%(len(data),url))

async_time_start = time.time()  # 开 始 时 间

gevent.joinall ([
    gevent.spawn(url_request,'https://www.python.org/'),
    gevent.spawn(url_request,'https://www.nginx.org/'),
    gevent.spawn(url_request,'https://www.ibm.com'),
  ])
print('haoshi:',time.time()-async_time_start)  # 总 用 时

协程 实现 多并发链接socket通信:

import socket,gevent
from gevent import monkey
monkey.patch_all()

def server_sock(port):
    s = socket.socket()
    s.bind(('',port))
    s.listen(10)
  while True:
    conn,addr = s.accept()
    gevent.spawn(handle_request,conn)

def handle_request(conn):
try:
    while True:
        data = conn.recv(1024)
        if not data: conn.shutdown(socket.SHUT_WR)
        print('recv:',data.decode())
        conn.send(data)
        
except Exception as ex:
    print(ex)
    
finally:
    conn.close()

if __name__ == '__main__':
server_sock(8888)

import socket

HOST = 'localhost'      # The remote host
PORT = 8888             # The same port as used by the server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
while True:
#msg = bytes(input(">>:"), encoding="utf8")
for i in range(50):
    s.send('dddd'.encode())
    data = s.recv(1024)
# print(data)

    print('Received', repr(data))
s.close()

回到顶部

3. 事件驱动

事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理,
另外两种常用的编程范式是 单线程同步 以及 多线程编程

服务器处理模型的程序时,有以下几种模型:

  • (1)每收到一个请求,创建一个新的进程,来处理该请求;
  • (2)每收到一个请求,创建一个新的线程,来处理该请求;
  • (3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求

第(1)中方法,由于创建新的进程的开销比较大,所以,会导致服务器性能比较差,但实现比较简单。

第(2)种方式,由于要涉及到线程的同步,有可能会面临 死锁 等问题。

第(3)种方式,在写应用程序代码时,逻辑比前面两种都复杂。

综合考虑各方面因素,一般普遍认为第(3)种方式是大多数网络服务器采用的方式

让我们用例子来比较和对比一下 单线程多线程 以及 事件驱动 编程模型。下图展示了随着时间的推移,这三种模式下程序所做的工作。这个程序有3个任务需要完成,每个任务都在等待I/O操作时阻塞自身。阻塞在I/O操作上所花费的时间已经用灰色框标示出来了。
在这里插入图片描述

在单线程同步模型中,任务按照顺序执行。如果某个任务因为I/O而阻塞,其他所有的任务都必须等待,直到它完成之后它们才能依次执行。

这种明确的执行顺序和串行化处理的行为是很容易推断得出的。如果任务之间并没有互相依赖的关系,但仍然需要互相等待的话这就使得程序不必要的降低了运行速度。

在多线程版本中,这3个任务分别在独立的线程中执行。这些线程由操作系统来管理,在多处理器系统上可以并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其他线程得以继续执行。与完成类似功能的同步程序相比,这种方式更有效率,但程序员必须写代码来保护共享资源,防止其被多个线程同时访问。多线程程序更加难以推断,因为这类程序不得不通过线程同步机制如锁、可重入函数、线程局部存储或者其他机制来处理线程安全问题,如果实现不当就会导致出现微妙且令人痛不欲生的bug。

在事件驱动版本的程序中,3个任务交错执行,但仍然在一个单独的线程控制中。当处理I/O或者其他昂贵的操作时,注册一个回调到事件循环中,然后当I/O操作完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询所有的事件,当事件到来时将它们分配给等待处理事件的回调函数。这种方式让程序尽可能的得以执行而不需要用到额外的线程。事件驱动型程序比多线程程序更容易推断出行为,因为程序员不需要关心线程安全问题。

当程序中有许多任务,且任务之间高度独立(它们不需要互相通信,或等待彼此)而且在等待事件到来时,某些任务会阻塞时事件驱动模型时个很好的选择;当应用程序需要在任务间共享可变的数据时,事件驱动模式可以更好的在单线程下处理。

网络应用程序通常都是上述特点,这使得它们能够很好的契合事件驱动编程模型。

此处要提出一个问题,就是,上面的事件驱动模型中,只要一遇到IO就注册一个事件,然后主程序就可以继续干其它的事情了,只到io处理完毕后,继续恢复之前中断的任务,这本质上是怎么实现的呢?这就涉及到select\poll\epoll异步IO

4. IO多路复用

同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?
不同的人在不同的上下文下给出的答案是不同的。所以先限定一下本文的上下文。
本文讨论的背景是Linux环境下的network IO。

在进行解释之前,首先要说明几个概念:
进程切换
进程的阻塞
文件描述符
缓存 I/O

进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。
因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。

从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:

  1. 保存处理器上下文,包括程序计数器和其他寄存器
  2. 更新PCB信息
  3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列
  4. 选择另一个进程执行,并更新其PCB
  5. 更新内存管理的数据结构
  6. 恢复处理器上下文

进程控制块PCB(Processing Control Block),是操作系统核心中一种数据结构,主要表示进程状态。

PCB的作用是使一个在多道程序环境下不能独立运行的程序(含数据),成为一个能独立运行的基本单位或与其它进程并发执行的进程。或者说,OS是根据PCB来对并发执行的进程进行控制和管理的。 PCB通常是系统内存占用区中的一个连续存区,它存放着操作系统用于描述进程情况及控制进程运行所需的全部信息

进程的阻塞

正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。

文件描述符fd

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。
当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

缓存 I/O

缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。
在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中。
数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 I/O 的缺点:
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

一个IO(如read)操作会经历以下两个阶段:

  1. 等待数据准备 (Waiting for the data to be ready)
  2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

因为有了这两个阶段,linux系统产生了下面五种网络模式的方案。

  • 1.阻塞 I/O(blocking IO)
  • 2.非阻塞 I/O(nonblocking IO)
  • 3.I/O 多路复用( IO multiplexing)
  • 4.信号驱动 I/O( signal driven IO)
  • 5.异步 I/O(asynchronous IO)

阻塞 I/O(blocking IO)

在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
在这里插入图片描述
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

非阻塞 I/O(nonblocking IO)

linux下,可通过设置socket使其变为非阻塞IO。当对一个non-blocking socket执行读操作时,流程是这个样子:
在这里插入图片描述
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。
从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
所以,nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。

I/O 多路复用( IO multiplexing)

IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。
select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。
它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket
当某个socket有数据到达了,就通知用户进程。
在这里插入图片描述
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
所以,I/O 多路复用的特点是通过一种机制使一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
IO多路复用和阻塞IO其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而阻塞IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个连接。
如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用多线程+阻塞IO的web server性能更好,可能延迟还更大。
select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking
但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

异步 I/O(asynchronous IO)

在这里插入图片描述
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。
然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

blocking 和 non-blocking 的区别:
调用blocking IO会一直block住对应的进程直到操作完成
调用non-blocking IO在kernel还准备数据的情况下会立刻返回

synchronous IO 和 asynchronous IO 的区别:
synchronous I/O操作会导致请求进程被阻塞,直到I/O操作完成;
asynchronous I/O操作不会导致请求进程被阻塞;

之前所说的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。

有人会说,non-blocking IO并没有被block啊。 这里需要格外注意,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。

而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。

各个IO Model的比较如图所示:
在这里插入图片描述
通过上面的图片,可以发现non-blocking IO和asynchronous IO的区别还是很明显的。

在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。

而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。

5. select、poll、epoll详解

select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制使一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的

异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

sellect、poll、epoll三者的区别 :

  • select:

目前支持几乎所有的平台

默认单个进程能够监视的文件描述符的数量存在最大限制,在linux上默认只支持1024个socket

可以通过修改宏定义或重新编译内核(修改系统最大支持的端口数)的方式提升这一限制

内核准备好数据后通知用户有数据了,但不告诉用户是哪个连接有数据,用户只能通过轮询的方式来获取数据

假定select让内核监视100个socket连接,当有1个连接有数据后,内核就通知用户100个连接中有数据了

但是不告诉用户是哪个连接有数据了,此时用户只能通过轮询的方式一个个去检查然后获取数据

这里是假定有100个socket连接,那么如果有上万个,上十万个呢?

那你就得轮询上万次,上十万次,而你所取的结果仅仅就那么1个。这样就会浪费很多没用的开销

只支持水平触发;每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也会很大

  • poll:

与select没有本质上的差别,仅仅是没有了最大文件描述符数量的限制

只支持水平触发

只是一个过渡版本,很少用

  • epoll:

Linux2.6才出现的epoll,具备了select和poll的一切优点,公认为性能最好的多路IO就绪通知方法

没有最大文件描述符数量的限制

同时支持水平触发和边缘触发

不支持windows平台

内核准备好数据以后会通知用户哪个连接有数据了

IO效率不随fd数目增加而线性下降

使用mmap加速内核与用户空间的消息传递

水平触发与边缘触发:
水平触发:
将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用epoll时将再次报告这些文件描述符,这种方式称为水平触发

边缘触发:
只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发

理论上边缘触发的性能要更高一些,但是代码实现相当复杂。

select和epoll的特点:

select:
select通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。

由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。

epoll:
epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。

另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

select
select(rlist,wlist,xlist,timeout=None)

select函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。

调用后select函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。

poll
int poll(struct pollfd *fds,unsigned int nfds,int timeout);

不同于select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现。

struct pollfd {
    int fd; /*file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。

从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。

事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

epool
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。

epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

epoll操作过程需要三个接口,分别如下:

int epoll_create(int size);
//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • (1) int epool_create(int size);
    创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。
    当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

  • (2)int epool_ctl(int epfd,int op,int fd,struct epoll_event event);
    函数是对指定描述符fd执行op操作。

    epfd: 是epoll_create()的返回值。
    
    op: 表示op操作,用三个宏来表示:
        添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。
        分别添加、删除和修改对fd的监听事件。
    
    fd: 是需要监听的fd(文件描述符)
    
    epoll_event:是告诉内核需要监听什么事
    
  • (3)int epoll_wait(int epfd, struct epoll_event events, int maxevents, int timeout);
    等待 epfd 上的 io 事件,最多返回 maxevents 个事件。
    参数 events 用来从内核得到事件的集合,maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create() 时的 size ,参数 timeout 是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如 返回0表示已超时。

一个简单的select多并发socket服务端代码:

import select
import socket
import queue

server = socket.socket()
HOST = 'localhost'
PORT = 8080
print("start up %s on port: %s",% (HOST,PORT))
server.bind((HOST,PORT))
server.listen()

server.setblocking(False)   # 不 阻 塞

msg_dic_queue = {}        # 这 是 一 个 队 列 字 典 ,存 放 要 返 回 给 客 户 端 的 数 据

inputs = [server]      # inputs 里 存 放 要 让 内 核 监 测 的 连 接 ,这 里 的 server 是 指 监 测server 本 身 的 连 接 状 态
#inputs = [server,conn]
outputs = []             # outputs 里 存 放 要 返 回 给 客 户 端 的 数 据 连 接 对 象

while True:
   print("waiting for next connect...")
   readable,writeable,exceptional = select.select(inputs,outputs,inputs)   #如果没有任何fd就绪,程序就会一直阻塞在这里
  # print(readable,writeable,exceptional)
for r in readable:         # 处 理 活 跃 的 连 接 , 每 个 r 就 是 一 个 socket 连 接 对 象
    if r is server:        # 代 表 来 了 一 个 新 连 接
        conn,client_addr = server.accept()
        print("arrived a new connect: ",client_addr)
        conn.setblocking(False)
        inputs.append(conn)   # 因 为 这 个 新 建 立 的 连 接 还 没 发 数 据 来 , 现 在 就 接 收 的 话 ,程 序 就 报 异 常 了
        
        # 所 以 要 想 实 现 这 个 客 户 端 发 数 据  来时 server 端 能 知 道,就 需 要 让 select 再 监 测 这 个 conn
        
        msg_dic_queue[conn] = queue.Queue()   # 初 始 化 一 个队 列 , 后 面 存 要 返 回 给 客 户 端 的 数 据
    else:                        # r 不 是 server 的 话 就 代 表 是 一 个 与 客 户 端 建 立 的 文 件 描 述  符了
        
        # 客 户 端 的 数 据 过 来 了 , 在 这 里 接 收 
       
        data = r.recv(1024)
        if data:
            print("received data from [%s]: "% r.getpeername()[0],data)
            msg_dic_queue[r].put(data)      # 收 到 的 数 据 先 放 到 队 列 字 典 里 , 之 后 再 返 回 给 客 户 端
            if r not in outputs:
                outputs.append(r)           # 放 入 返 回 的 连 接 队 列 里 。 为 了 不 影 响 处 理 与 其 它 客 户 端 的 连 接 , 这 里 不 立 刻 返 回 数 据 给 客 户 端
        else:                                   # 如 果 收 不 到 data 就 代 表 客 户 端 已 经 断 开 了
            print("Client is disconnect",r)
            if r in outputs:
                outputs.remove(r)            # 清 理 已 断 开 的 连 接
            inputs.remove(r)
            del msg_dic_queue[r]

for w in writeable:                     # 处 理 要 返 回 给 客 户 端 的 连 接 列 表
    try:
        next_msg = msg_dic_queue[w].get_nowait()
    except queue.Empty:
        print("client [%s]"% w.getpeername()[0],"queue is empty...")
        outputs.remove(w)      # 确 保 下 次循 环 时 writeable 不 返 回 已 经 处 理 完 的 连 接
    else:
        print("sending message to [%s]"% w.getpeername()[0],next_msg)
        w.send(next_msg)        # 返 回 给 客 户 端 源 数 据

for e in exceptional:            # 处 理 异 常 连 接
    if e in outputs:
        outputs.remove(e)
    inputs.remove(e)
    del msg_dic_queue[e]

select多并发socket客户端代码:

import socket

msgs = [ b'This is the message. ',
         b'It will be sent ',
         b'in parts.',
         ]
SERVER_ADDRESS = 'localhost'
SERVER_PORT = 8080

#Create a few TCP/IP socket

socks = [ socket.socket(socket.AF_INET, socket.SOCK_STREAM) for i in range(500) ]

#Connect the socket to the port where the server is listening

print('connecting to %s port %s' % (SERVER_ADDRESS,SERVER_PORT))
for s in socks:
s.connect((SERVER_ADDRESS,SERVER_PORT))

for message in msgs:

# Send messages on both sockets

for s in socks:
    print('%s: sending "%s"' % (s.getsockname(), message) )
    s.send(message)

# Read responses on both sockets
for s in socks:
    data = s.recv(1024)
    print( '%s: received "%s"' % (s.getsockname(), data) )
    if not data:
        print(sys.stderr, 'closing socket', s.getsockname() )

epoll多并发socket服务端代码如下:

import socket, logging
import select, errno

logger = logging.getLogger("network-server")

def InitLog():
    logger.setLevel(logging.DEBUG)

    fh = logging.FileHandler("network-server.log")
    fh.setLevel(logging.DEBUG)
    ch = logging.StreamHandler()
    ch.setLevel(logging.ERROR)

  formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
  ch.setFormatter(formatter)
  fh.setFormatter(formatter)

  logger.addHandler(fh)
  logger.addHandler(ch)


if __name__ == "__main__":
InitLog()

try:
    # 创 建 TCP socket 作 为 监 听 socket
    
    listen_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
except socket.error as  msg:
    logger.error("create socket failed")

try:
    # 设 置 SO_REUSEADDR 选 项
    
    listen_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
except socket.error as  msg:
    logger.error("setsocketopt SO_REUSEADDR failed")

try:
    # 进行 bind -- 此处未指定 ip 地址,即 bind 了全部网卡 ip 上
    listen_fd.bind(('', 8008))
except socket.error as  msg:
    logger.error("bind failed")

try:
    # 设置 listen 的 backlog 数
    listen_fd.listen(10)
except socket.error as  msg:
    logger.error(msg)

try:
    # 创建 epoll 句柄
    epoll_fd = select.epoll()
    # 向 epoll 句柄中注册 监听 socket 的 可读 事件
    epoll_fd.register(listen_fd.fileno(), select.EPOLLIN)
except select.error as  msg:
    logger.error(msg)

connections = {}
addresses = {}
datalist = {}
while True:
    # epoll 进行 fd 扫描的地方 -- 未指定超时时间则为阻塞等待
    epoll_list = epoll_fd.poll()

    for fd, events in epoll_list:
        # 若为监听 fd 被激活
        if fd == listen_fd.fileno():
            # 进行 accept -- 获得连接上来 client 的 ip 和 port,以及 socket 句柄
            conn, addr = listen_fd.accept()
            logger.debug("accept connection from %s, %d, fd = %d" % (addr[0], addr[1], conn.fileno()))
            # 将连接 socket 设置为 非阻塞
            conn.setblocking(0)
            # 向 epoll 句柄中注册 连接 socket 的 可读 事件
            epoll_fd.register(conn.fileno(), select.EPOLLIN | select.EPOLLET)
            # 将 conn 和 addr 信息分别保存起来
            connections[conn.fileno()] = conn
            addresses[conn.fileno()] = addr
        elif select.EPOLLIN & events:
            # 有 可读 事件激活
            datas = ''
            while True:
                try:
                    # 从激活 fd 上 recv 10 字节数据
                    data = connections[fd].recv(10)
                    # 若当前没有接收到数据,并且之前的累计数据也没有
                    if not data and not datas:
                        # 从 epoll 句柄中移除该 连接 fd
                        epoll_fd.unregister(fd)
                        # server 侧主动关闭该 连接 fd
                        connections[fd].close()
                        logger.debug("%s, %d closed" % (addresses[fd][0], addresses[fd][1]))
                        break
                    else:
                        # 将接收到的数据拼接保存在 datas 中
                        datas += data
                except socket.error as  msg:
                    # 在 非阻塞 socket 上进行 recv 需要处理 读穿 的情况
                    # 这里实际上是利用 读穿 出 异常 的方式跳到这里进行后续处理
                    if msg.errno == errno.EAGAIN:
                        logger.debug("%s receive %s" % (fd, datas))
                        # 将已接收数据保存起来
                        datalist[fd] = datas
                        # 更新 epoll 句柄中连接d 注册事件为 可写
                        epoll_fd.modify(fd, select.EPOLLET | select.EPOLLOUT)
                        break
                    else:
                        # 出错处理
                        epoll_fd.unregister(fd)
                        connections[fd].close()
                        logger.error(msg)
                        break
        elif select.EPOLLHUP & events:
            # 有 HUP 事件激活
            epoll_fd.unregister(fd)
            connections[fd].close()
            logger.debug("%s, %d closed" % (addresses[fd][0], addresses[fd][1]))
        elif select.EPOLLOUT & events:
            # 有 可写 事件激活
            sendLen = 0
            # 通过 while 循环确保将 buf 中的数据全部发送出去
            while True:
                # 将之前收到的数据发回 client -- 通过 sendLen 来控制发送位置
                sendLen += connections[fd].send(datalist[fd][sendLen:])
                # 在全部发送完毕后退出 while 循环
                if sendLen == len(datalist[fd]):
                    break
            # 更新 epoll 句柄中连接 fd 注册事件为 可读
            epoll_fd.modify(fd, select.EPOLLIN | select.EPOLLET)
        else:
            # 其他 epoll 事件不进行处理
            continue

5、python之selectors模块

selectors模块是在python3.4版本中引进的,它封装了IO多路复用中的select和epoll,能够更快,更方便的实现多并发效果。
官方文档见:https://docs.python.org/3/library/selectors.html
以下是一个selectors模块的代码示范:

import selectors
import socket

#selectors模块默认会用epoll,如果你的系统中没有epoll(比如windows)则会自动使用select

sel = selectors.DefaultSelector()   #生成一个select对象

def accept(sock, mask):
 conn, addr = sock.accept()  # Should be ready
 print('accepted', conn, 'from', addr)
 conn.setblocking(False) #设定非阻塞
 sel.register(conn, selectors.EVENT_READ, read)  #新连接注册read回调函数

def read(conn, mask):
   data = conn.recv(1024)  # Should be ready
   if data:
      print('echoing', repr(data), 'to', conn)
      conn.send(data)
   else:
      print('closing', conn)
      sel.unregister(conn)
      conn.close()

sock = socket.socket()
sock.bind(('localhost', 8080))
sock.listen()
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept)    #把刚生成的sock连接对象注册到select连接列表中,并交给accept函数处理

  while True:
    events = sel.select()   #默认是阻塞,有活动连接就返回活动的连接列表
    
    #这里看起来是select,其实有可能会使用epoll,如果你的系统支持epoll,那么默认就是epoll

for key, mask in events:
    callback = key.data     #去调accept函数
    callback(key.fileobj, mask) #key.fileobj就是readable中的一个socket连接对象


1.事件:

事件是一个可以让我们在Greenlet之间异步通信的形式贴上一个gevent指南上面的例子:

import gevent
from gevent.event import Event
evt = Event()

def setter():
   print('A: Hey wait for me, I have to do something')
   gevent.sleep(3)
   print("Ok, I'm done")
   evt.set()

def waiter():
  print("I'll wait for you")
  evt.wait()                # blocking 阻塞   
  print("It's about time")

def main():
gevent.joinall([
    gevent.spawn(setter),
    gevent.spawn(waiter),
    gevent.spawn(waiter),
])

if __name__ == '__main__': main()

在这里插入图片描述
这里setter和waiter一共起了三个协程。分析一下运行顺序应该很容易了解evt是干嘛的:

首先回调之行到运行setter 打印str然后gevent.sleep(3)。

然后执行第二个回调waitter()执行到evt.wait()的时候阻塞住然后切换,怎么切换的细节要分析的话又是一大波。总之就是切换了

然后执行第三个回调waitter()执行到evt.wait()又被阻塞了,这个时候继续执行下一个回调就会回到setter里面,因为没有人在他前面往hub.loop里注册了

然后这里执行"ok, i’m done" ok我撸完了,运行evt.set()将flag设置为True.

然后另外两个被阻塞的waitter的evt.wait()方法在看到flag已经为True之后不再继续阻塞运行并且结束。

可以看到,Event可以协同好几个Greenlet同时工作,并且一个主Greenlet在操作的时候可以让其他几个都处于等待的状态,可以实现一些特定的环境和需求。

import gevent
from gevent.event import AsyncResult

a = AsyncResult()
def setter():
    gevent.sleep(3)
    a.set('Hello!')

def waiter():
    print(a.get())  # 处于阻塞状态

gevent.joinall([
gevent.spawn(setter),
gevent.spawn(waiter),
])

在这里插入图片描述

import gevent
from gevent.event import AsyncResult
a = AsyncResult()
def setter():
    gevent.sleep(3)
    a.set('Hello!')

def waiter():
    print("waiter")
    print(a.get())

gevent.joinall ([
    gevent.spawn(setter),
    gevent.spawn(waiter),
  ])

在这里插入图片描述
Event还有一个扩展 AsyncResult ( 异步 ),
这个扩展可以在 set 的时候带上数据传递给各 waiter 去 get。
这里get还是会阻塞,但是等待的就是不flag了,而是一个值或一个报错相关更详细的api

2. 队列:

队列是一个排序的数据集合,它有常见的 put / get 操作, 但是它是可以在Greenlet之间可以安全操作的方式来实现的。

举例来说,如果一个Greenlet从队列中取出一项,此项就不会被同时执行的其它Greenlet再取到了。
可以理解成基于greenlet之间的安全队列吧还是先贴上一个官方的例子:

import gevent
from gevent.queue import Queue

 tasks = Queue()   # 创建一个队列

def worker(n):
   while not tasks.empty():
     task = tasks.get()
     print('Worker %s got task %s' % (n, task))
     gevent.sleep(0)   # 相当于不休眠
   print('Quitting time!')  # 当列表空了,就打印结束了

def boss():
    for i in xrange(1,25):
        tasks.put_nowait(i)  # 是以一种非堵塞的方式往队列里放值;直接 put 相当于阻塞

gevent.spawn(boss).join()  # 创建了一个协程,并挂起

   gevent.joinall ([    
   gevent.spawn(worker, 'steve'),     # 第一协程
   gevent.spawn(worker, 'john'),      # 第二协程
   gevent.spawn(worker, 'nancy'),     # 第三协程
 ])

在这里插入图片描述

首先初始化一个 Queue() 实例。这里会先运行boss() 调用put_nowait()方法
不阻塞的往队列里面放24个元素。然后下面依次从Queue里对数字进行消费,
起了三个协程分别命名了不同的名字,使用get方法依次消费队列里面的数字直到消费完毕。

put 和 get 操作都有 非阻塞的版本put_nowaitget_nowait 不会阻塞,前者是 put 的非阻塞版本,后者是 get 的非阻塞版本
然而在操作不能完成时抛出 gevent.queue.Emptygevent.queue.Full 异常。
同时Queue队列可以支持设置最大队列值,查看队列现在元素数量qsize(),队列是否为空empty(),
队列是否满了full()等api在使用的时候最好

3.Group / Pool gevent文档翻译为组合池:

组(group) 是一个运行中 greenlet 的集合,集合中的 greenlet 像一个组一样 会被共同管理和调度。
它也兼饰了像 Python 的 multiprocessing库 那样的 平行调度器的角色。

我的理解是,在一个组(group)里面的 greenlet 会被统一管理和调度。

先看指南上的例子:

import gevent
from gevent.pool import Group

def talk(msg):
for i in xrange(3):
    print(msg)

g1 = gevent.spawn(talk, 'bar')
g2 = gevent.spawn(talk, 'foo')

group = Group()
group.add(g1)
group.add(g2)
group.join()

print("主线程")

在这里插入图片描述
就是 spawn 了好几个 talk,然后都加到组里面。最后使用 group.join()
等待所有 spawn 完成,每完成一个就会从group里面去掉。
在这里插入图片描述
由于没有返回值等问题,这个demo非常简单,

第一个例子Group().map():

from gevent import getcurrent
from gevent.pool import Group

group = Group()
def hello_from(n):
    print('Size of group %s' % len(group))
    print('Hello from Greenlet %s' % id(getcurrent()))  # 获 取 当 前 协 程 的 id 值
    return n

x = group.map(hello_from, xrange(3)) 
# 相 当 于 这 个 group 里 面 有 三 个 值 ,先传3,再传2,再传1。 默认值是 0 到 3
# 这个group.map里面放了很多东西,不仅创建 hello_from 这个协程,还传值到x

print (type(x))
print (x) 

看下图打印结果可以看出,虽然是三个值执行一个方法 hello_from ,但是他们打印出的结果可以看出,执行的地址各不一样
在这里插入图片描述

这里使用了 group.map() 这个函数来取得各 spawn 的返回值。map() 是由第二个参数控制迭代次数,并且传递给第一个参数值而运行的。拿这个函数举例,
这里会返回一个 list 构成这个 list 的对象就是将迭代的参数传进函数运行之后的返回值。
这里得到的结果是:[0, 1, 2]

第二个例子Group().imap():

import gevent
from gevent.pool import Group

def intensive(n):
    gevent.sleep(3 - n)
    return 'task', n

print('Ordered')
ogroup = Group()
x = ogroup.imap(intensive, range(3))
print x
print(type(x))

for x in ogroup.imap(intensive, xrange(3)):
  print x

在这里插入图片描述

这里 imap 与 map 不一样的地方可能熟悉 python基础库 的很容易看出来,
map 返回 list 对象(列表对象),而 imap 返回一个 iterable 对象(迭代器对象)。

所以如果要取得里面的值比如想打印就必须写成像代码最后一行那种。(或者直接包一个list让他变成map函数)。

另外提一下 imap 的内部实现,其实是继承了 Greenlet对象,在里面启了 spawn()。

imap 里面还有一个挺好用的参数 maxsize 默认情况是没有设置的当设置了之后,
会将迭代变成一批一批的执行,

这里再举一个例子:

def intensive(n):
    gevent.sleep(2)
    return 'task', n

print('Ordered')
ogroup = Group()
x = ogroup.imap(intensive, xrange(20), maxsize=3)
print (x)
print(type(x))
for x in ogroup.imap(intensive, range(3)):
    print(x)

这里运行的时候,会将并行控制到3个,执行也是每2秒执行3个,而不是不设置的时候2秒之后将输出所有的结果。

第三个例子 Group().imap_unordered():

这个就很厉害了,我们直接上例子:

import gevent
from gevent.pool import Group

def intensive(n):
    gevent.sleep(3 - n)   # 第一个协程过来,休眠三秒;第二个协程过来休眠一两秒;总之,三个协程不是同时出的!
    return 'task', n
    
igroup = Group()
for i in igroup.imap_unordered(intensive, range(3)):
    print(i)

在这里插入图片描述

运行了可以看到输出是:

(‘task’, 2)
(‘task’, 1)
(‘task’, 0)

先返回的先回来,这个如果是imap运行的话,会先等上3秒钟开始返回0然后1 2 一次返回。

最后我们再谈一下Pool对象,指南上的例子没啥意思。

Group是Pool类的父类。pool是可以指定池子里面最多可以拥有多少greenlet在跑而且申明也很简单:

from gevent.pool import Pool

x = Pool(10)   # 十个协程池,加协程池可以对这个池子进行统一管理

其他就是继承了一些Group中的用法.

最后我用一个我利用这一章中讲解到的一些数据结构写的生产消费者模型结束gevent数据结构及实战三的讲解:

import gevent
import gevent.monkey
gevent.monkey.patch_all()
import requests
from gevent.queue import Queue, Full, Empty
from gevent.pool import Pool


# if Queue() have no parameter It's unlimited
# out jd_queue just put in 100 msg.......
msg_queue = Queue(100)
jd_pool = Pool(10)
jd_msg = "Boom"
test_url = "http://www.xiachufang.com"


def deal_with():
while True:
    try:
        now_id = gevent.getcurrent()
        msg = msg_queue.get_nowait()
        print "handle " + msg
        print 'now start with now_id: %s' % now_id
        requests.get(test_url)
        print 'now end with now_id: %s' % now_id
    except Empty:
        gevent.sleep(0)


def product_msg(jd_msg):
while True:
    try:
        msg_queue.put_nowait(jd_msg)
        print msg_queue.qsize()
    except Full:
        gevent.sleep(5)


jd_pool.add(gevent.spawn(product_msg, jd_msg))
for i in xrange(10):
jd_pool.add(gevent.spawn(deal_with))
jd_pool.join()
  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值