24 IO多路复用and异步非阻塞and协程

#先来记下概念性东西

  IO多路复用作用:检测所有IO请求(主要是socket)是否已经发生变化(是否已经连接成功/是否已经获取数据)(可读/可写)

  同步:按顺序执行

  阻塞:等待

  异步:执行完成之后自动执行回掉函数或自动执行某些操作(通知)

  非阻塞:不等待

  协程:本身是个没什么用的东西,一般跟IO操作一起使用。协程的作用就是进行分片,使得线程在代码快之间按照你的需求来切换

  执行,而不是原来的逐行执行。

  

1、如果我们要用实现socket的并发请求。除了用多线程和多进程的。单线程也能实现:

  1、使用基于事件循环的IO多路复用+非阻塞,也可以达到并发效果。分别访问三个网址,拿到返回(如果拿到后有后续操作那么就是异步了)。这里用到select模块

  原始代码如下:

import socket
import select

client1 = socket.socket()
client1.setblocking(False)
try:
    client1.connect(('www.baidu.com', 80))
except BlockingIOError as e:
    pass

client2 = socket.socket()
client2.setblocking(False)
try:
    client2.connect(('www.sougou.com', 80))
except BlockingIOError as e:
    pass

client3 = socket.socket()
client3.setblocking(False)
try:
    client3.connect(('so.m.sm.cn', 80))
except BlockingIOError as e:
    pass

socket_list = [client1, client2, client3]
conn_list = [client1, client2, client3]
while True:
    rlist,wlist,elist = select.select(socket_list, conn_list, [], 0.005)
    '''
    一共四个参数。
    第一个参数。socket_list,检测服务端是否返回数据---可读
    第二个参数。conn_list,检测其中的socket是否已经和服务端连接成功---可写
    第三个参数。[],用来检测异常
    第四个参数。每次检测时间间隔
    '''
    for sk in wlist:
        if sk == client1:
            sk.sendall(b'GET /s?wd=alex HTTP/1.0
host:www.baidu.com

')
        elif sk == client2:
            sk.sendall(b'GET /web?query=alex HTTP/1.0
host:www.sogou.com

')
        else:
            sk.sendall(b'GET /s?q=alex HTTP/1.0
host:so.m.sm.cn

')
        conn_list.remove(sk)

    for sk in rlist:
        chunk_list = []
        while True:
            try:
                chunk = sk.recv(8096)
                if not chunk:
                    break
                chunk_list.append(chunk)
            except BlockingIOError as e:
                break
        body = b''.join(chunk_list)
        print('-------->', body)
        #print(body.decode('utf-8'))
        sk.close()
        socket_list.remove(sk)

    if not socket_list:
        break

  封装版本如下:

# coding:utf-8
import socket
import select


class Req(object):
    def __init__(self, sk, func):
        self.sock = sk
        self.func = func

    def fileno(self):
        return self.sock.fileno()


class NB(object):

    def __init__(self):
        self.conn_list = []
        self.socket_list = []

    def add(self, url, func):
        client = socket.socket()
        client.setblocking(False)  # 非阻塞
        try:
            client.connect((url, 80))
        except BlockingIOError as e:
            pass
        obj = Req(client, func)
        self.conn_list.append(obj)
        self.socket_list.append(obj)

    def run(self):
        while True:
            rlist,wlist,elist = select.select(self.socket_list, self.conn_list, [], 0.005)
            # wlist中表示已经连接成功的req对象
            for sk in wlist:
                # sk:发生变化的req对象
                sk.sock.sendall(b'GET /s?wd=alex HTTP/1.0
host:www.baidu.com

')
                self.conn_list.remove(sk)

            for sk in rlist:
                chunk_list = []
                while True:
                    try:
                        chunk = sk.sock.recv(8096)
                        if not chunk:
                            break
                        chunk_list.append(chunk)
                    except BlockingIOError as e:
                        break
                body = b''.join(chunk_list)
                sk.func(body)
                sk.sock.close()
                self.socket_list.remove(sk)

            if not self.socket_list:
                break


def baidu_response(body):
    print('百度下载的结果:', body)


def sogou_response(body):
    print('搜狗下载的结果:', body)


def so_response(body):
    print('uc下载的结果:', body)


t1 = NB()
t1.add('www.baidu.com', baidu_response)
t1.add('www.sogou.com', sogou_response)
t1.add('so.m.sm.cn', so_response)
t1.run()

  注意点:

    1、socket阻塞之后要捕获BlockingIOError异常。

    2、对比同步阻塞。异步非阻塞请求发过去之后,继续发下一个请求,不会等待消耗时间。然后回来的时候排队。但是比起多线程还是差距挺大的。

2、协程:

  单独协程用到greenlet模块

  如下简单demo

import greenlet


def f1():
    print(11)  # 1、打印这里
    gr2.switch()
    print(22)   # 3、打印这里
    gr2.switch()


def f2():
    print(33)   # 2、打印这里
    gr1.switch()
    print(44)    # 4、打印这里


gr1 = greenlet.greenlet(f1)  # 协程gr1
gr2 = greenlet.greenlet(f2)   # 协程gr2
gr1.switch()

  所以说单独的协程没用。下面是配合IO切换来使用。这里用到了monkey模块

from gevent import monkey
monkey.patch_all()  # 下面代码中遇到IO都会自动执行greenlet的switch进行切换
import requests
import gevent

def get_page_1(url):
    ret = requests.get(url)
    print(url, ret.content)


def get_page_2(url):
    ret = requests.get(url)
    print(url, ret.content)


def get_page_3(url):
    ret = requests.get(url)
    print(url, ret.content)

gevent.joinall([
    gevent.spawn(get_page_1, 'https://www.python.org/'),  # 协程1
    gevent.spawn(get_page_2, 'https://www.yahoo.com/'),  # 协程2
    gevent.spawn(get_page_3, 'https://www.github.com/'),  # 协程3
])

  这里就比单独来回访问三次要快得多。由上面的例子可得。协程+IO切换和基于事件循环的IO多路复用(有个Twisted框架)本质差不太多。

原文地址:https://www.cnblogs.com/cbslock/p/11388853.html