TOP

python-day35(协程,IO多路复用)

一. 协程

  协程: 是单线程下的并发,又称微线程,纤程,英文名Coroutine. 

  并发: 切换+保存状态

    协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的

  协程特点:

    1. 必须在只有一个单线程里实现并发

    2. 修改共享数据不需要加锁

    3. 用户程序自己保存多个控制流的上线文栈

    4. 一个协程遇到IO操作自动切换到其他协程(如何实现检测IO,yield,greenlet都无法实现,

      就用到了gevent模块(select机制))

  Greenlet模块

    单纯的切换(在没有IO的情况下或者没有重复开辟内存空间的操作),反而会降低程序的执行速度

  #真正的协程模块就是使用greenlet完成的切换
from greenlet import greenlet

def eat(name):
    print('%s eat 1' %name)  #2
    g2.switch('taibai')   #3
    print('%s eat 2' %name) #6
    g2.switch() #7
def play(name):
    print('%s play 1' %name) #4
    g1.switch()      #5
    print('%s play 2' %name) #8

g1=greenlet(eat)
g2=greenlet(play)

g1.switch('taibai')#可以在第一次switch时传入参数,以后都不需要  1
#安装
pip install greenlet
 1 # import time
 2 #
 3 # import greenlet
 4 # def fun1():
 5 #     time.sleep(2)
 6 #     print('约吗?')
 7 #     g2.switch()
 8 #     time.sleep(2)
 9 #     print('不约')
10 #     g2.switch()
11 # def fun2():
12 #     time.sleep(2)
13 #     print('你好')
14 #     g1.switch()
15 #     time.sleep(2)
16 #     print('你不好')
17 #
18 # g1 = greenlet.greenlet(fun1)
19 # g2 = greenlet.greenlet(fun2)
20 # g1.switch()
21 # ----------------------------------------------------
22 # import time
23 # import greenlet
24 # def f1(name):
25 #     print(name)
26 #     time.sleep(2)
27 #     a = g2.switch(name+'1')
28 #     print(a)
29 #
30 # def f2(name):
31 #     print(name)
32 #     time.sleep(2)
33 #     g1.switch(name+'1')
34 #
35 # a = time.time()
36 # g1 = greenlet.greenlet(f1)
37 # g2 = greenlet.greenlet(f2)
38 # g1.switch('hui')
39 # b = time.time()
40 # print(b-a)
greenlet练习

 

  Gevent模块

#安装
pip install gevent
#用法
g1 = gevent.spawn(func,多个参数) 创建一个协程对象g1, spawn(函数名,多个参数)
spawn是异步提交任务

g2 = gevent.spawn(func2)

g1.join() #等待g1结束,上面只是创建协程对象,这个join才是去执行
g2.join() #等待g2结束,

gevent.joinall([g1,g2]) #等待列表中的协程都结束
g1.value #拿到func1的返回值
gevent.sleep(2)# 睡2秒 遇到IO切换
# 不能识别time.sleep 解决办法 from gevent import monkey;monkey.patch_all()

  遇到IO阻塞是自动切换任务

import gevent
def eat(name):
    print('%s eat 1' %name)
    gevent.sleep(2)
    print('%s eat 2' %name)

def play(name):
    print('%s play 1' %name)
    gevent.sleep(1)
    print('%s play 2' %name)


g1=gevent.spawn(eat,'egon')
g2=gevent.spawn(play,name='egon')
g1.join()
g2.join()
#或者gevent.joinall([g1,g2])
print('')

  gevent.sleep(2)模拟的是gevent可以识别的io阻塞,

  而time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要下面一行的代码,打补丁,就可以识别

  from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前

  或者直接记忆成: 要用gevent, 需要将from gevent import monkey;monkey.patch_all()放到文件的开头

 1 # import gevent
 2 # import time
 3 # #gevent 不能识别 time.sleep
 4 # # 解决办法 from gevent import monkey;monkey.patch_all() 识别所有的IO 自动切换
 5 #
 6 # def func(n,):
 7 #     print('xxxx',n)
 8 #     gevent.sleep(2)
 9 #     # time.sleep(2) #不能识别
10 #     print('ccccc')
11 #
12 # def func2(m):
13 #     print('11111111',m)
14 #     gevent.sleep(2)
15 #     print('22222222')
16 # g1 = gevent.spawn(func,'alex')  #  异步提交任务
17 # g2 = gevent.spawn(func2,'马来西亚')
18 #
19 # # gevent.joinall([g1,g2]) #等多所有
20 # g1.join() #执行并等待异步提交的任务完成
21 # g2.join()#执行并等待异步提交的任务完成
22 # print('代码结束')
from gevent import monkey;monkey.patch_all()

 

二. IO多路复用

  IO模型

#1)  等待数据准备(Waiting for the data to be ready)
#2)  将数据从内核拷贝到进程中(Copying the data from the kernel to the process)
#1、输入操作:read、readv、recv、recvfrom、recvmsg共5个函数,如果会阻塞状态,
  则会经理wait data和copy data两个阶段,如果设置为非阻塞则在wait 不到data时抛出异常 #2、输出操作:write、writev、send、sendto、sendmsg共5个函数,
  在发送缓冲区满了会阻塞在原地,如果设置为非阻塞,则会抛出异常 #3、接收外来链接:accept,与输入操作类似 #4、发起外出链接:connect,与输出操作类似
 

  1. 阻塞IO

  

blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。

  这里我们回顾一下同步/异步/阻塞/非阻塞:

    同步:提交一个任务之后要等待这个任务执行完毕

    异步:只管提交任务,不等待这个任务执行完毕就可以去做其他的事情

    阻塞:recv、recvfrom、accept,线程阶段  运行状态-->阻塞状态-->就绪

    非阻塞:没有阻塞状态

  在一个线程的IO模型中,我们recv的地方阻塞,我们就开启多线程,但是不管你开启多少个线程,'

  这个recv的时间是不是没有被规避掉,不管是多线程还是多进程都没有规避掉这个IO时间。

  2. 非阻塞IO模型

    

    非阻塞IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有

 1 # 服务端
 2 import socket
 3 import time
 4 
 5 server=socket.socket()
 6 server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
 7 server.bind(('127.0.0.1',8083))
 8 server.listen(5)
 9 
10 server.setblocking(False) #设置不阻塞
11 r_list=[]  #用来存储所有来请求server端的conn连接
12 w_list={}  #用来存储所有已经有了请求数据的conn的请求数据
13 
14 while 1:
15     try:
16         conn,addr=server.accept() #不阻塞,会报错
17         r_list.append(conn)  #为了将连接保存起来,不然下次循环的时候,上一次的连接就没有了
18     except BlockingIOError:
19         # 强调强调强调:!!!非阻塞IO的精髓在于完全没有阻塞!!!
20         # time.sleep(0.5) # 打开该行注释纯属为了方便查看效果
21         print('在做其他的事情')
22         print('rlist: ',len(r_list))
23         print('wlist: ',len(w_list))
24 
25 
26         # 遍历读列表,依次取出套接字读取内容
27         del_rlist=[] #用来存储删除的conn连接
28         for conn in r_list:
29             try:
30                 data=conn.recv(1024) #不阻塞,会报错
31                 if not data: #当一个客户端暴力关闭的时候,会一直接收b'',别忘了判断一下数据
32                     conn.close()
33                     del_rlist.append(conn)
34                     continue
35                 w_list[conn]=data.upper()
36             except BlockingIOError: # 没有收成功,则继续检索下一个套接字的接收
37                 continue
38             except ConnectionResetError: # 当前套接字出异常,则关闭,然后加入删除列表,等待被清除
39                 conn.close()
40                 del_rlist.append(conn)
41 
42 
43         # 遍历写列表,依次取出套接字发送内容
44         del_wlist=[]
45         for conn,data in w_list.items():
46             try:
47                 conn.send(data)
48                 del_wlist.append(conn)
49             except BlockingIOError:
50                 continue
51 
52 
53         # 清理无用的套接字,无需再监听它们的IO操作
54         for conn in del_rlist:
55             r_list.remove(conn)
56         #del_rlist.clear() #清空列表中保存的已经删除的内容
57         for conn in del_wlist:
58             w_list.pop(conn)
59         #del_wlist.clear()
60 
61 #客户端
62 import socket
63 import os
64 import time
65 import threading
66 client=socket.socket()
67 client.connect(('127.0.0.1',8083))
68 
69 while 1:
70     res=('%s hello' %os.getpid()).encode('utf-8')
71     client.send(res)
72     data=client.recv(1024)
73 
74     print(data.decode('utf-8'))
75 
76 
77 ##多线程的客户端请求版本
78 # def func():
79 #     sk = socket.socket()
80 #     sk.connect(('127.0.0.1',9000))
81 #     sk.send(b'hello')
82 #     time.sleep(1)
83 #     print(sk.recv(1024))
84 #     sk.close()
85 #
86 # for i in range(20):
87 #     threading.Thread(target=func).start()
非阻塞io示例

 

  3. IO多路复用

  

 强调:

    1. 如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,

      可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

    2. 在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。

      只不过process是被select这个函数block,而不是被socket IO给block。

  python中的select模块:

import select

fd_r_list, fd_w_list, fd_e_list = select.select(rlist, wlist, xlist, [timeout])

参数: 可接受四个参数(前三个必须)
    rlist: wait until ready for reading  #等待读的对象,你需要监听的需要获取数据的对象列表
    wlist: wait until ready for writing  #等待写的对象,你需要写一些内容的时候,input等等,也就是说我会循环他看看是否有需要发送的消息,如果有我取出这个对象的消息并发送出去,一般用不到,这里我们也给一个[]。
    xlist: wait for an “exceptional condition”  #等待异常的对象,一些额外的情况,一般用不到,但是必须传,那么我们就给他一个[]。
    timeout: 超时时间
    当超时时间 = n(正整数)时,那么如果监听的句柄均无任何变化,则select会阻塞n秒,之后返回三个空列表,如果监听的句柄有变化,则直接执行。
返回值:三个列表与上面的三个参数列表是对应的
  select方法用来监视文件描述符(当文件描述符条件不满足时,select会阻塞),当某个文件描述符状态改变后,会返回三个列表
    1、当参数1 序列中的fd满足“可读”条件时,则获取发生变化的fd并添加到fd_r_list中
    2、当参数2 序列中含有fd时,则将该序列中所有的fd添加到 fd_w_list中
    3、当参数3 序列中的fd发生错误时,则将该发生错误的fd添加到 fd_e_list中
    4、当超时时间为空,则select会一直阻塞,直到监听的句柄发生变化
 1 #服务端
 2 from socket import *
 3 import select
 4 server = socket(AF_INET, SOCK_STREAM)
 5 server.bind(('127.0.0.1',8093))
 6 server.listen(5)
 7 # 设置为非阻塞
 8 server.setblocking(False)
 9 
10 # 初始化将服务端socket对象加入监听列表,后面还要动态添加一些conn连接对象,当accept的时候sk就有感应,当recv的时候conn就有动静
11 rlist=[server,]
12 rdata = {}  #存放客户端发送过来的消息
13 
14 wlist=[]  #等待写对象
15 wdata={}  #存放要返回给客户端的消息
16 
17 print('预备!监听!!!')
18 count = 0 #写着计数用的,为了看实验效果用的,没用
19 while True:
20     # 开始 select 监听,对rlist中的服务端server进行监听,select函数阻塞进程,直到rlist中的套接字被触发(在此例中,套接字接收到客户端发来的握手信号,从而变得可读,满足select函数的“可读”条件),被触发的(有动静的)套接字(服务器套接字)返回给了rl这个返回值里面;
21     rl,wl,xl=select.select(rlist,wlist,[],0.5)
22     print('%s 次数>>'%(count),wl)
23     count = count + 1
24     # 对rl进行循环判断是否有客户端连接进来,当有客户端连接进来时select将触发
25     for sock in rl:
26         # 判断当前触发的是不是socket对象, 当触发的对象是socket对象时,说明有新客户端accept连接进来了
27         if sock == server:
28             # 接收客户端的连接, 获取客户端对象和客户端地址信息
29             conn,addr=sock.accept()
30             #把新的客户端连接加入到监听列表中,当客户端的连接有接收消息的时候,select将被触发,会知道这个连接有动静,有消息,那么返回给rl这个返回值列表里面。
31             rlist.append(conn)
32         else:
33             # 由于客户端连接进来时socket接收客户端连接请求,将客户端连接加入到了监听列表中(rlist),客户端发送消息的时候这个连接将触发
34             # 所以判断是否是客户端连接对象触发
35             try:
36                 data=sock.recv(1024)
37                 #没有数据的时候,我们将这个连接关闭掉,并从监听列表中移除
38                 if not data:
39                     sock.close()
40                     rlist.remove(sock)
41                     continue
42                 print("received {0} from client {1}".format(data.decode(), sock))
43                 #将接受到的客户端的消息保存下来
44                 rdata[sock] = data.decode()
45 
46                 #将客户端连接对象和这个对象接收到的消息加工成返回消息,并添加到wdata这个字典里面
47                 wdata[sock]=data.upper()
48                 #需要给这个客户端回复消息的时候,我们将这个连接添加到wlist写监听列表中
49                 wlist.append(sock)
50             #如果这个连接出错了,客户端暴力断开了(注意,我还没有接收他的消息,或者接收他的消息的过程中出错了)
51             except Exception:
52                 #关闭这个连接
53                 sock.close()
54                 #在监听列表中将他移除,因为不管什么原因,它毕竟是断开了,没必要再监听它了
55                 rlist.remove(sock)
56     # 如果现在没有客户端请求连接,也没有客户端发送消息时,开始对发送消息列表进行处理,是否需要发送消息
57     for sock in wl:
58         sock.send(wdata[sock])
59         wlist.remove(sock)
60         wdata.pop(sock)
61 
62     # #将一次select监听列表中有接收数据的conn对象所接收到的消息打印一下
63     # for k,v in rdata.items():
64     #     print(k,'发来的消息是:',v)
65     # #清空接收到的消息
66     # rdata.clear()
67 
68 ---------------------------------------
69 #客户端
70 from socket import *
71 
72 client=socket(AF_INET,SOCK_STREAM)
73 client.connect(('127.0.0.1',8093))
74 
75 
76 while True:
77     msg=input('>>: ').strip()
78     if not msg:continue
79     client.send(msg.encode('utf-8'))
80     data=client.recv(1024)
81     print(data.decode('utf-8'))
82 
83 client.close()
select网络IO模型的示例代码

 

  4. 异步IO

  貌似异步IO这个模型很牛~~但是你发现没有,这不是我们自己代码控制的,都是操作系统完成的,而python在copy数据这个阶段没有提供操纵操作系统的接口,所以用python没法实现这套异步IO机制,其他几个IO模型都没有解决第二阶段的阻塞(用户态和内核态之间copy数据),但是C语言是可以实现的,因为大家都知道C语言是最接近底层的,虽然我们用python实现不了,但是python仍然有异步的模块和框架(tornado、twstied,高并发需求的时候用),这些模块和框架很多都是用底层的C语言实现的,它帮我们实现了异步,你只要使用就可以了,但是你要知道这个异步是不是很好呀,不需要你自己等待了,操作系统帮你做了所有的事情,你就直接收数据就行了,就像你有一张银行卡,银行定期给你打钱一样。

 >>>>>>>>更多协程

>>>>>>>>更多IO操作

posted on 2018-12-04 19:39  hui_T  阅读(230)  评论(0编辑  收藏  举报