• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
KK筑梦人
博客园    首页    新随笔    联系   管理    订阅  订阅

协程(gevent模块、greenlet模块、urllib模块)、IO模型(阻塞IO、非阻塞IO模型、IO多路复用模型、selector模块) 第三十六天 2018.11.26

协程:

进程、线程、协程比较:

  进程   启动多个进程  进程之间是由操作系统负责调用

  线程   启动多个线程  真正被CPU执行的最小单位实际是线程

       开启一个线程  创建一个线程  寄存器 堆栈

       关闭一个线程

  协程(爬虫 socket)

       本质上是一个线程(可以理解为虚拟线程、帮助来实现任务遇到IO时在线程间的切换)

       能够在多个任务之间切换来节省一些IO时间

       协程中任务之间的切换也消耗时间,但是开销要远远小于进程线程之间的切换

进程和线程的任务切换右操作系统完成

协程任务之间的切换由程序(代码)完成,只有遇到协程模块能识别的IO操作的时候,程序才会进行任务切换,实现并发的效果

协程遇到IO就切换---->无认识的IO不切换

如果没遇到可识别的IO,程序就是顺序执行,就没有并发效果

通过生成器实现协程并行运算:

实现并发的手段,在多个任务间来回切换

import time
def consumer():
    while True:            # 开启无限循环
        x = yield
        time.sleep(1)
        print('处理了数据 :',x)

def producer():
    c = consumer()           # 将c赋值为consumer生成器函数
    next(c)                  # 生成器的next方法和send方法相互配合
    for i in range(10):     # next()触发生成器函数
        time.sleep(1)
        print('生产了数据 :',i)
        c.send(i)           # 触发生成器,把i赋值给光标停留前的yield

producer()

day9的生成器实现协程并行运算(生产者消费者模型) 

import time
def consumer(name):
    print('我是[%s],我准备开始吃包子了' %name)
    while True:
        baozi=yield           #第二次之后遇到yield停留时间程序立刻跳到c1.send
                              #获得第i个包子
        time.sleep(1)          #吃包子时间间隔
        print('%s 很开心的把[%s]吃掉了' %(name,baozi))
 
def producer():
    c1=consumer('wupeiqi')     #顾客名字及顾客个数
    c2=consumer('yuanhao_SB')
    c1.__next__()               #可以换成c1.send(0)
    c2.__next__()               #可以换成c2.send(0)
    for i in range(1,10):        #顾客消费数目
        time.sleep(1)           #制作时间间隔
        c1.send('猪肉馅包子 %s' %i)     #把括号里的赋值给yield
        c2.send('牛肉馅包子 %s' %i)
producer()         #风湿理论---->函数即变量
#有消费(需求)才有生产(制作)
#两个函数一起执行----->并发   A->B->A->B->A->B
#每个函数通过一个循环控制,一个循环终止时另一个循环终止

协程模块---->greenlet模块

单纯的切换---->遇到IO原地阻塞---->先将函数注册给greenlet,再通过使用switch进行来回切换

from greenlet import greenlet
def eat():
    print('eating start')
    g2.switch()     # 切换到g2
    print('eating end')
    g2.switch()

def play():
    print('playing start')
    g1.switch()     # 切换回g1
    print('playing end')
g1 = greenlet(eat)    # 函数注册给greenlet
g2 = greenlet(play)   
g1.switch()      # 切换

协程模块---->gevent模块  

同步  和  异步

gevent.spawn(func,1,2,3,x=4,y=5)创建一个协程对象g1,spawn括号内第一个参数是函数名,后面可以传多个参数

gevent模块无法识别其他IO,需要通过monkey模块调用monkey对象的patch_all()方法将所有IO模拟成gevent模块可以识别的IO

gevent.sleep()模拟的是gevent可以识别的IO阻塞

time.sleep()和其他阻塞无法被gevent识别,因此需要加monkey打补丁,就可以识别了

from gevent import monkey;monkey.patch_all() 

必须放到被打补丁者的前面,如time、socket模块之前

from gevent import monkey;monkey.patch_all()   会把patch_all之后所有模块的阻塞都打成一个包,打完包后,gevent模块就可以感知到其他模块的IO操作了

用threading.current_thread().getName()来查看每个协程对象,查看的结果为DummyThread-n,即假线程

from gevent import monkey;monkey.patch_all()        # 会把patch_all之后所有模块的阻塞都打成一个包
import time                                         # 打完包后,gevent模块就可以感知到time.sleep()
import gevent         # 此模块感知不到time.sleep()
import threading
def eat():
    print(threading.current_thread().getName())
    print(threading.current_thread())
    print('eating start')
    time.sleep(1)                  # 执行到此,遇到IO跳到play函数
    print('eating end')

def play():
    print(threading.current_thread().getName())    # 查看线程名---->线程看来一个假的线程就是协程
    print(threading.current_thread())              # id号为虚拟线程的id
    print('playing start')
    time.sleep(1)                  # 从eat函数跳来执行到此,遇到IO跳回eat函数
    print('playing end')

g1 = gevent.spawn(eat)   # 创建一个eat函数的协程对象
g2 = gevent.spawn(play)  # 创建一个play函数的协程对象
g1.join()                # 等待g1结束
g2.join()                # 等待g2结束

'''
DummyThread-1
<_DummyThread(DummyThread-1, started daemon 64339232)>
eating start
DummyThread-2
<_DummyThread(DummyThread-2, started daemon 64339808)>
playing start
eating end
playing end
'''

gevent模块---->协程间的同步异步

from gevent import monkey;monkey.patch_all()  # 会把patch_all之后所有模块的阻塞都打成一个包
import time                                   # 打完包后,gevent模块就可以感知到time.sleep()
import gevent    # 此模块感知不到time.sleep()

def task(n):
    time.sleep(1)
    print(n)

def sync():           # 同步
    for i in range(10):
        task(i)

def kk():             # 异步
    g_lst = []
    for i in range(10):
        g = gevent.spawn(task,i)   # 异步协程---->调用函数、传参
        g_lst.append(g)
    gevent.joinall(g_lst)  # for g in g_lst:g.join()   # .join等待结束
    # gevent.joinall(g_lst) == for g in g_lst:g.join()
sync()                # 同步控制
kk()                  # 异步控制
# 都为顺序输出0-9

协程---->并发、切换效率快  

遇到IO时间切换到另一个线程规避IO时间

并发 = 进程+线程+协程

    5      20    500

协程 : 能够在一个线程中实现并发效果的概念

     能够规避一些任务中的IO操作

     在任务的执行过程中,检测到IO就切换到其他任务

多线程 被弱化了

协程   在一个线程上   提高CPU 的利用率

协程相比于多线程的优势   切换的效率更快

协程---->urllib模块---->爬虫

爬虫的例子

正则基础

请求过程中的IO等待

获取数据的过程

from gevent import monkey;monkey.patch_all()
import gevent           # 引入协程
# import requests       # 需要安装的
from urllib.request import urlopen    # 内置的模块
def get_url(url):
    response = urlopen(url)                     # 获取所爬取网页的数据
    # response2 = requests.get(url)
    content = response.read().decode('utf-8')  # 完全按照格式来的html---->有格式的
    # response2.content.decode('utf-8')        # 拿到一个简化版---->写在一行(无格式)
    return len(content)      # 返回content的长度

g1 = gevent.spawn(get_url,'http://www.baidu.com')     # spawn大量生产
g2 = gevent.spawn(get_url,'http://www.sogou.com')     # 方法  参数
g3 = gevent.spawn(get_url,'http://www.taobao.com')    # 创建一个协程对象
g4 = gevent.spawn(get_url,'http://www.hao123.com')
g5 = gevent.spawn(get_url,'http://www.cnblogs.com')
gevent.joinall([g1,g2,g3,g4,g5])       # 等待所有的都结束
print(g1.value)
print(g2.value)       # .value拿到返回值
print(g3.value)       # .join等待结束
print(g4.value)
print(g5.value)

ret = get_url('http://www.baidu.com')
print(ret)

协程---->socketserver 

 

server端:

# 在线程之间的切换是操作系统级别
# 在协程之间的操作是代码级别
from gevent import monkey;monkey.patch_all()
import socket
import gevent
def talk(conn):             # 把可以进行变化操作的设置为一个函数
    conn.send(b'hello')
    print(conn.recv(1024).decode('utf-8'))
    conn.close()

sk = socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()
while True:
    conn,addr = sk.accept()
    gevent.spawn(talk,conn) # 创建一个talk函数协程对象,传入的参数为连接
sk.close()

client端:

import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8080))
print(sk.recv(1024))
msg = input('>>>').encode('utf-8')
sk.send(msg)
sk.close()

IO模型: 

笔记:

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

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

阻塞   recv recvfrom accept

非阻塞

 

阻塞    线程   运行状态   -->   阻塞状态 -->   就绪

非阻塞

IO多路复用

  select机制     Windows linux 都是操作系统轮询每一个被监听的项,看是否有读操作

  poll机制         linux 它可以监听的对象比select机制可以监听的多

    随着监听项的增多,导致效率降低

  epoll机制       linux 给每个对象绑定了一个回调函数---->不通过操作系统的轮询,直接通过回调函数效率更高

selectors模块,能够自动选择适合的IO多路复用

IO模型介绍: 

    Stevens在文章中一共比较了五种IO Model:
    * blocking IO           阻塞IO
    * nonblocking IO      非阻塞IO
    * IO multiplexing      IO多路复用
    * signal driven IO     信号驱动IO
    * asynchronous IO    异步IO
    由signal driven IO(信号驱动IO)在实际中并不常用,所以主要介绍其余四种IO Model。

一个IO的两个阶段:

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

IO模型---->阻塞IO  

IO模型---->非阻塞IO

server端:

# while True---->异常占内存---->及其浪费cpu
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.setblocking(False)        # 设置为非阻塞
sk.listen()
# 通过两个列表---->连接列表和删除断开连接的列表
# 连接列表把每次连接存储起来
# 删除列表用来记录断开的连接,从而按删除列表里的元素删除连接列表里的无用元素
conn_l = []        # 连接列表
del_conn = []      # 删除已经断开的连接---->每次删除完连接列表里的元素,清空删除列表
while True:
    try:
        conn,addr = sk.accept()       #不阻塞,但是没人连我会报错
        print('建立连接了:',addr)
        conn_l.append(conn)
    except BlockingIOError:
        for con in conn_l:
            try:
                msg = con.recv(1024)  # 非阻塞,如果没有数据就报错
                if msg == b'':       # tcp协议不可以发送空消息
                    del_conn.append(con)
                    continue
                print(msg)
                con.send(b'byebye')
            except BlockingIOError:pass
        for con in del_conn:   # for循环里不能删除本循环的项,否则会造成本循环索引错误
            con.close()
            conn_l.remove(con)
        del_conn.clear()
# while True : 10000   500  501

client端:  

import time
import socket
import threading
def func():
    sk = socket.socket()
    sk.connect(('127.0.0.1',9000))
    sk.send(b'hello')
    time.sleep(1)
    print(sk.recv(1024))
    sk.close()

for i in range(2):
    threading.Thread(target=func).start()  # 利用线程异步执行多个任务

IO模型---->IO多路复用  

server端:

# 未使用牺牲CPU复用率来达到
# 通过操作系统的代理来实现监听从而达到
import select
import socket

sk = socket.socket()
sk.bind(('127.0.0.1',8000))
sk.setblocking(False)      # 设置socket的接口为非阻塞
sk.listen()

read_lst = [sk]   # 监听sk对象、conn连接
while True:      # [sk,conn]
    r_lst,w_lst,x_lst = select.select(read_lst,[],[])
    # 读事件 写事件 条件修改
    # r_lst里面只有要接收的连接/sk对象
    for i in r_lst:
        if i is sk:           # sk对象
            conn,addr = i.accept()     # sk.accept()
            read_lst.append(conn)
        else:                 # conn连接
            ret = i.recv(1024)
            if ret == b'':
                i.close()
                read_lst.remove(i)    # 在列表中删除已经断开的连接
                continue
            print(ret)
            i.send(b'goodbye!')

# client端断开连接后会一直给server端发送空消息,直到server端断开连接

client端: 

import time
import socket
import threading
def func():
    sk = socket.socket()
    sk.connect(('127.0.0.1',8000))
    sk.send(b'hello')
    time.sleep(3)
    print(sk.recv(1024))
    sk.close()

for i in range(20):
    threading.Thread(target=func).start()

IO模型---->异步IO  

Python暂时实现不了

selectors模块:

自动帮你选择合适的IO多路复用模型(select,poll,epoll)

IO多路复用

  select机制     Windows linux 都是操作系统轮询每一个被监听的项,看是否有读操作

  poll机制         linux 它可以监听的对象比select机制可以监听的多

    随着监听项的增多,导致效率降低

  epoll机制       linux 给每个对象绑定了一个回调函数---->不通过操作系统的轮询,直接通过回调函数效率更高

selectors模块,能够自动选择适合的IO多路复用

import selectors
from socket import *

def accept(sk,mask):
    conn,addr=sk.accept()
    sel.register(conn,selectors.EVENT_READ,read)    # conn连接对象、可读事件、执行read

def read(conn,mask):
    try:
        data=conn.recv(1024)
        if not data:                 # 如果没有接收到client端的消息
            print('closing',conn)
            sel.unregister(conn)     # 注销代理所监听的conn对象
            conn.close()             # 关闭连接
            return
        conn.send(data.upper()+b'_SB')
    except Exception:
        print('closing', conn)
        sel.unregister(conn)         # 没有接收消息(进入阻塞)也关闭
        conn.close()

sk=socket()
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)    # 就是他在bind前面加,防止端口号被上次使用后占用
sk.bind(('127.0.0.1',8088))
sk.listen(5)                               # tcp队列的最大连接数,大部分设置为5
sk.setblocking(False)                      # 设置socket的接口为非阻塞
sel=selectors.DefaultSelector()            # 选择一个适合我的IO多路复用的机制
sel.register(sk,selectors.EVENT_READ,accept)   # 监听的是一个读事件     sk对象、可读事件、accept的回调函数
# EVENT_READ :      表示可读的; 它的值其实是1;
# EVENT_WRITE:      表示可写的; 它的值其实是2;
# 相当于网select的读列表里append了一个sk对象,并且绑定了一个回调函数accept
# 说白了就是 如果有人请求连接sk,就调用accrpt方法

while True:
    events=sel.select()           # 检测所有的sk,conn,是否有完成wait data阶段
    for sel_obj,mask in events:  # [conn]    sk   连接
        callback=sel_obj.data    # callback=read
        callback(sel_obj.fileobj,mask) # read(conn,1)

'''
register(fileobj, events, data=None)      作用:注册一个文件对象。
                                          参数: fileobj——即可以是fd 也可以是一个拥有fileno()方法的对象; 
                                                 events——上面的event Mask 常量; data
                                          返回值: 一个SelectorKey类的实例;
unregister(fileobj)                       作用: 注销一个已经注册过的文件对象;      
                                          返回值:一个SelectorKey类的实例;
select(timeout=None)                    作用: 用于选择满足我们监听的event的文件对象;
                                        返回值: 是一个(key, events)的元组, 
                                        其中key是一个SelectorKey类的实例, 而events 就是 event
Mask(EVENT_READ或EVENT_WRITE,或者二者的组合) 
'''

  

 

posted @ 2018-11-26 23:52  KK筑梦人  阅读(316)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3