IO多路复用/基于IO多路复用+socket实现并发请求/协程

http://www.cnblogs.com/alex3714/articles/5876749.html

http://www.cnblogs.com/Eva-J/articles/8324837.html

http://www.cnblogs.com/linhaifeng/articles/6817679.html

一.IO多路复用

1.IO多路复用作用:检测多个socket是否已经发生变化(是否已经连接成功/是否已经获取数据)(可读/可写)

2.select,poll,epoll都是I/O多路复用的具体的实现,之所以有这三个鬼东西的存在,其实是他们出现是有先后顺序的

(1).I/O多路复用这个概念被提出来以后,select是第一个实现的

   select会修改传入的参数数组,只能监听1024个链接

   select不是线程安全的

(2).14年以后,一帮人又实现了poll,poll修复了select的很多问题

  poll去掉了1024个链接的限制,于是要多少链接呐,你开心就好

   poll从设计上来说,不再修改传入的数据,不过这个要看你的平台,所以行走江湖,小心为妙

(3).2002年,大设备部Davide Libenzi实现了epoll

  epoll可以说是I/O多路复用最新的实现,epoll修复了poll和select绝大部分问题

  epoll现在是线程安全的

  epoll现在不仅告诉你sock组里有数据,还会告诉你哪个sock组有数据

 比较坑爹的是epoll只用Linux提供支持,默认集成到了Linux内核中

Windows Python:
    提供:select

Mac Python:
    提供:select

Linux Python:
    提供:select,poll,epoll

对于select操作:

句柄列表11, 句柄列表22, 句柄列表33 = select.select(句柄序列1, 句柄序列2, 句柄序列3, 超时时间)

参数: 可接受四个参数(前三个必须)
返回值:三个列表

select方法用来监视文件句柄,如果句柄发生变化,则获取该句柄。
1、当 参数1 序列中的句柄发生可读时(accetp和read),则获取发生变化的句柄并添加到 返回值1 序列中
2、当 参数2 序列中含有句柄时,则将该序列中所有的句柄添加到 返回值2 序列中
3、当 参数3 序列中的句柄发生错误时,则将该发生错误的句柄添加到 返回值3 序列中
4、当 超时时间 未设置,则select会一直阻塞,直到监听的句柄发生变化
     当 超时时间 = 1时,那么如果监听的句柄均无任何变化,则select会阻塞 1 秒,之后返回三个空列表,如果监听的句柄有变化,则直接执行。

二.基于I/O多路复用+socket实现并发请求(一个线程100个请求)

应用功能:I/O多路复用;socket非阻塞

1.以前所使用的socket发送请求:

import socket
import requests

# 方式一
ret = requests.get('https://www.baidu.com/s?wd=alex')

# 方式二
client = socket.socket()

# 百度创建连接: 阻塞
client.connect(('www.baidu.com',80))

# 问百度我要什么?
client.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')    # http协议

# 我等着接收百度给我的回复
chunk_list = []
while True:
    chunk = client.recv(8096)
    if not chunk:
        break
    chunk_list.append(chunk)

body = b''.join(chunk_list)
print(body.decode('utf-8'))
View Code
import socket
import requests
##### 解决并发:单线程 #####
# 方式一
key_list = ['alex','db','sb']
for item in key_list:
    ret = requests.get('https://www.baidu.com/s?wd=%s' %item)

# 方式二
def get_data(key):
    # 方式二
    client = socket.socket()

    # 百度创建连接: 阻塞
    client.connect(('www.baidu.com',80))

    # 问百度我要什么?
    client.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')

    # 我等着接收百度给我的回复
    chunk_list = []
    while True:
        chunk = client.recv(8096)
        if not chunk:
            break
        chunk_list.append(chunk)

    body = b''.join(chunk_list)
    print(body.decode('utf-8'))

key_list = ['alex','db','sb']
for item in key_list:
    get_data(item)


##### 解决并发:多线程 #####
import threading

key_list = ['alex','db','sb']
for item in key_list:
    t = threading.Thread(target=get_data,args=(item,))
    t.start()

# #################### 解决并发:单线程+IO不等待 ####################
# IO请求?
# 数据回来了?
向百度发送请求搜索三个关键字

2.使用I/O多路复用+socket实现

import socket

client = socket.socket()
client.setblocking(False) # 将原来阻塞的位置变成非阻塞(报错)
# 百度创建连接: 阻塞

try:
    client.connect(('www.baidu.com',80)) # 执行了但报错了
except BlockingIOError as e:
    pass

# 检测到已经连接成功

# 问百度我要什么?
client.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')

# 我等着接收百度给我的回复
chunk_list = []
while True:
    chunk = client.recv(8096) # 将原来阻塞的位置变成非阻塞(报错)
    if not chunk:
        break
    chunk_list.append(chunk)

body = b''.join(chunk_list)
print(body.decode('utf-8'))
前戏(会报错)
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.sogou.com',80))
except BlockingIOError as e:
    pass


client3 = socket.socket()
client3.setblocking(False) # 淘宝创建连接: 非阻塞
try:
    client3.connect(('www.taobao.com',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)
    # wlist中表示已经连接成功的socket对象
    for sk in wlist:
        if sk == client1:
            sk.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')
        elif sk==client2:
            sk.sendall(b'GET /web?query=fdf HTTP/1.0\r\nhost:www.sogou.com\r\n\r\n')
        else:
            sk.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.taobao.com\r\n\r\n')
        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.decode('utf-8'))
        print('------------>',body)
        sk.close()
        socket_list.remove(sk)
    if not socket_list:
        break
完美(单线程的并发)
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:
                # 发生变换的req对象
                sk.sock.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')
                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)
                # print(body.decode('utf-8'))
                sk.func(body)
                sk.sock.close()
                self.socket_list.remove(sk)
            if not self.socket_list:
                break


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

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

def cnblogs_repsonse(body):
    print('博客园下载结果:', body)


t1 = Nb()
t1.add('www.baidu.com',baidu_repsonse)
t1.add('www.sogou.com',sogou_repsonse)
t1.add('www.cnblogs.com',cnblogs_repsonse)
t1.run()
高级版(线程的并发)

3.基于事件循环实现的异步非阻塞框架:

(1).yfz自定义框架

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:
                # 发生变换的req对象
                sk.sock.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')
                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)
                # print(body.decode('utf-8'))
                sk.func(body)
                sk.sock.close()
                self.socket_list.remove(sk)
            if not self.socket_list:
                break
yfz

(2).使用:

from yfz import Nb

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

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

def oldboyedu_repsonse(body):
    print('老男孩下载结果:', body)

t1 = Nb()
t1.add('www.baidu.com',baidu_repsonse)
t1.add('www.sogou.com',sogou_repsonse)
t1.add('www.oldboyedu.com',oldboyedu_repsonse)
t1.run()
使用

 

4.总结:

  (1).IO多路复用的作用:

    检查多个socket是否发生变化

    操作系统检测socket是否发生变化,有三种模式:

     select:最多1024个socket;循环去检测

     poll:不限制监听socket个数;循环去检测(水平触发)

     epoll:不限制监听socket个数;回调方式(边缘触发)

    Python模块:

     select.select

     select.epoll

  (2).提高并发的方案:

    多进程

    多线程

    异步非阻塞模块(Twisted)  scrapy框架(单线程完成并发)

  (3).什么是异步非阻塞

    非阻塞,不等待:

      比如创建socket对某个地址进行connect,获取接收数据recv时默认都会等待(连接成功或接收到数据),才执行后续操作

      如果设置setblocking(False),以上两个过程就不在等待,但是会报BlockingIOError的错误,只要捕获即可

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

      比如做爬虫中向某个地址baidu.com发送请求,当请求执行完成之后自动执行回调函数

  (4).什么是同步阻塞?

    阻塞:等

    同步:按照顺序逐步执行

key_list = ['alex','db','sb']
for item in key_list:
    ret = requests.get('https://www.baidu.com/s?wd=%s' %item)
    print(ret.text)
同步阻塞

  (5).概念:

# 你写的代码:7000w
v = [
    [11,22], # 每个都有一个append方法
    [22,33], # 每个都有一个append方法
    [33,44], # 每个都有一个append方法
]


# 王思聪
for item in v:
    print(item.append)
之前
class Foo(object):
    def __init__(self,data,girl):
    self.row = data
        self.girl = girl
    def append(self,item):
    self.row.append(item)

v = [
        Foo([11,22],'雪梨'), # 每个都有一个append方法
    Foo([22,33],'冰糖'), # 每个都有一个append方法
    Foo([33,44],'糖宝'), # 每个都有一个append方法
]

for item in v:
    print(item.append)
    item.girl
之后

三.协程

概念:

  进程,操作系统中存在

  线程,操作系统中存在

  协程,是由程序员创造出来的一个不是真实存在的东西

协程:是微线程,对线程进程分片,使得线程在代码块之间进行来回切换执行,而不是在原来逐步执行

1.Greenlet模块

安装:pip3 install greenlet

import greenlet

def f1():
    print(11)
    gr2.switch()
    print(22)
    gr2.switch()

def f2():
    print(33)
    gr1.switch()
    print(44)


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

gr1.switch()
greenlet实现状态切换

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

单纯的协程无用

2.Gevent模块

协程 + 遇到IO就切换 => 牛逼起来了

(1).安装:pip3 install gevent

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

(2).用法:

g1=gevent.spawn(func,1,,2,3,x=4,y=5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的

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

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

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

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

gevent.joinall([
    gevent.spawn(get_page1, 'https://www.python.org/'), # 协程1
    gevent.spawn(get_page2, 'https://www.yahoo.com/'),  # 协程2
    gevent.spawn(get_page3, 'https://github.com/'),     # 协程3
])
遇到IO主动切换

3.总结:

(1).什么是协程?

  协程也可以称为"微线程",就是开发者控制线程执行流程,控制先执行某段代码然后再切换到另外函数执行代码...来回切换

(2).协程可以提高并发吗?

  协程自己本身无法实现并发(甚至性能会降低)

  协程+IO切换性能提高

(3).进程,线程,协程的区别

  进程是计算机资源分配的最小单元,主要用来做数据隔离,线程是计算机工作的最小单元,一个进程中可以有多个线程,一个应用程序可以有多个进程;在其他语言中,有进程这个概念但是不常用,都是用的线程,而在python 中IO密集型操作用线程; 计算密集型用进程,因为在python中存在一个gil锁,在一个进程中同一时刻只有一个线程可以被cpu调度,如果想利用cpu的多核优势,就开多进程,协程本身是一个不存在的东西,是由程序员创造出的一个概念,对一个线程分片,使得线程在代码块之间进行来回切换,而不是原来的逐行执行,单纯的协程并没有意义,协程和io切换结合可以提高性能,一般使用gevent模块.

(4).单线程提供并发:

  协程+IO切换:gevent

  基于事件循环的异步非阻塞框架:Twisted

四.基于yield实现协程:

def f1():
    print(11)
    yield
    print(22)
    yield
    print(33)

def f2():
    print(55)
    yield
    print(66)
    yield
    print(77)

v1 = f1()
v2 = f2()

next(v1) # v1.send(None)
next(v2) # v1.send(None)
next(v1) # v1.send(None)
next(v2) # v1.send(None)
next(v1) # v1.send(None)
next(v2) # v1.send(None)
View Code
posted @ 2018-09-13 18:07  骑驴老神仙  阅读(423)  评论(0)    收藏  举报