返回顶部

11 python 协程和异步io

并发、并行、同步、异步、阻塞、非阻塞

并发:是指一个时间段内,有几个程序在同一个cpu上运行,但是任意时刻只有一个cpu上运行

并行:是指任意时刻点上,有多个程序同时运行在多个cpu上

同步:是指代码调用IO操作时,必须等待IO操作完成才返回的调用方式

异步:是指代码调用IO操作时,不必等待IO操作完成就返回的调用方式

阻塞:是指调用函数时候当前线程被挂起

非阻塞:是指调用函数时候当前线程不会被挂起,而是立即返回

IO 多路复用 (select、poll 和 epoll)

C10K问题

如何在1Gcpu,2G内存,1g网络环境下,让单台服务器同时为1万个客户端提供FTP服务

 Unix下五中I/O模型

  阻塞式I/O

  非阻塞式I/O

  I/O多路复用

  信号驱动I/O

  异步I/O

                         阻塞式I/O

在等待数据返回的时候存在大量的时间等待过程

 

                        非阻塞式I/O

虽然在遇到I/O操作是不在阻塞,但是需要CPU不断的去询问是否已成功准备好,消耗大量CPU操作,在数据准备好后,把数据从内核赋值到用户空间还是要阻塞

 

 

             I/O多路复用

 虽然在遇到I/O操作时,他也是处于阻塞的状态,但是它和阻塞I/O的区别是,它可以监听多个socket,如果有一个socket的状态发生变化会立即返回处理,即把要监听的多个socket放到一个列表中监听

主要有3种模型:select poll epoll 

epoll并不代表一定比select好

  在并发高的情况下,连接活跃度不是很高, epoll比select
  并发性不高,同时连接很活跃, select比epoll好

                       异步IO

 遇到IO操作时立即返回,等待数据从内核赋值到用户内存空间后,在通知程序来执行,无IO等待时间

 

 

select+回调+事件循环获取html

非阻塞io虽然在遇到io操作时不会等待,但会消耗大量的cpu进行询问,在数据从内核赋值到用户空间还是有一定的等待时间。

和同步的等待时间没什么区别,但是它可以在阻塞的时候除了不停的询问外,还可以在别的事情

#1. epoll并不代表一定比select好
# 在并发高的情况下,连接活跃度不是很高, epoll比select
# 并发性不高,同时连接很活跃, select比epoll好

#通过非阻塞io实现http请求

import socket
from urllib.parse import urlparse


#使用非阻塞io完成http请求

def get_url(url):
    #通过socket请求html
    url = urlparse(url)
    host = url.netloc
    path = url.path
    if path == "":
        path = "/"

    #建立socket连接
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.setblocking(False)
    try:
        client.connect((host, 80)) #阻塞不会消耗cpu
    except BlockingIOError as e:
        pass

    #不停的询问连接是否建立好, 需要while循环不停的去检查状态
    #做计算任务或者再次发起其他的连接请求

    while True:
        try:
            client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8"))
            break
        except OSError as e:
            pass

    data = b""
    while True:
        try:
            d = client.recv(1024)
        except BlockingIOError as e:
            continue
        if d:
            data += d
        else:
            break

    data = data.decode("utf8")
    html_data = data.split("\r\n\r\n")[1]
    print(html_data)
    client.close()

if __name__ == "__main__":
    get_url("http://www.baidu.com")
通过非阻塞io实现http请求

 同步http请求和select+回调+时间循环请求对比

同步http请求20个url

import socket
from urllib.parse import urlparse

def get_url(url):
    #通过socket请求html
    url = urlparse(url)
    host = url.netloc
    path = url.path
    if path == "":
        path = "/"

    #建立socket连接
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    client.connect((host, 80)) #阻塞不会消耗cpu

    client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8"))

    data = b""
    while True:
        d = client.recv(1024)
        if d:
            data += d
        else:
            break

    data = data.decode("utf8")
    html_data = data.split("\r\n\r\n")[1]
    print(html_data)
    client.close()

if __name__ == "__main__":
    import time
    start_time = time.time()
    for url in range(20):
        url = "http://shop.projectsedu.com/goods/{}/".format(url)
        get_url(url)
    print(time.time()-start_time)

 执行时间如下

select+回调+时间循环请求20个url

回调+事件循环+select(poll\epoll) 是目前各大框架底层常用的IO多路复用技术

1 事件循环,不停的请求socket的状态并调用对应的回调函数,通过loop事件循环  不断循环检测ready是否有返回值   ready = selector.select()  ready有返回值的话执行对应的回调函数 

2. select本身是不支持register模式, selectors在select进行了封装可以把socket注册到select中

  selector.register("socket文件类型", 事件类型(读/写), socket建立成功后的回调函数地址)

3. socket状态变化以后的回调是由程序员完成的

import socket
from urllib.parse import urlparse
from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE


selector = DefaultSelector()
#使用select完成http请求
urls = []
stop = False


class Fetcher:
    def connected(self, key):
        selector.unregister(key.fd)
        self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(self.path, self.host).encode("utf8"))
        selector.register(self.client.fileno(), EVENT_READ, self.readable)

    def readable(self, key):
        d = self.client.recv(1024)
        if d:
            self.data += d
        else:
            selector.unregister(key.fd)
            data = self.data.decode("utf8")
            html_data = data.split("\r\n\r\n")[1]
            print(html_data)
            self.client.close()
            urls.remove(self.spider_url)
            if not urls:
                global stop
                stop = True

    def get_url(self, url):
        self.spider_url = url
        url = urlparse(url)
        self.host = url.netloc
        self.path = url.path
        self.data = b""
        if self.path == "":
            self.path = "/"

        # 建立socket连接
        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.client.setblocking(False)

        try:
            self.client.connect((self.host, 80))  # 阻塞不会消耗cpu
        except BlockingIOError as e:
            pass

        #注册
        selector.register(self.client.fileno(), EVENT_WRITE, self.connected)


def loop():
    #事件循环,不停的请求socket的状态并调用对应的回调函数
    while not stop:
        ready = selector.select()
        for key, mask in ready:
            call_back = key.data
            call_back(key)

if __name__ == "__main__":
    fetcher = Fetcher()
    import time
    start_time = time.time()
    for url in range(20):
        url = "http://shop.projectsedu.com/goods/{}/".format(url)
        urls.append(url)
        fetcher = Fetcher()
        fetcher.get_url(url)
    loop()
    print(time.time()-start_time)

 执行时间如下

 

 

协程是什么 

回调虽然有很到的性能,但是对于我们而言也面临了不少的问题

1 可读性差

2 共享状态管理困难

3 异常处理困难 

 我们即想使用回调的高性能,又想使用起来简单,所以就有了协程

 协成类似于一个可以暂停的函数(可以向暂停的地方传入值),可以在一个线程中进行来回切换

 

生成器进阶-send、close和throw方法

 send 传值给生成器 yield产出值

def gen_func():
    #1. 可以产出值, 2. 可以接收值(调用方传递进来的值)
    html = yield "http://projectsedu.com"
    print(html)
    return "bobby"

if __name__ == "__main__":
    gen = gen_func()
    #在调用send发送非none值之前,我们必须启动一次生成器, 方式有两种1. gen.send(None), 2. next(gen)
    url = gen.send(None)
    #download url
    html = "bobby"
    print(gen.send(html)) #send方法可以传递值进入生成器内部,同时还可以重启生成器执行到下一个yield位置
    print(gen.send(html))  # 抛出异常 StopIteration 

输出结果如下

close关闭生成器

def gen_func():
    #1. 可以产出值, 2. 可以接收值(调用方传递进来的值)
    try:
        yield "http://projectsedu.com"
    except RuntimeError:
        pass

    yield 2
    yield 3
    return "bobby"

if __name__ == "__main__":
    gen = gen_func()
    print(next(gen))
    gen.close()  # 关闭生成器
    print("bobby")

    #GeneratorExit是继承自BaseException, Exception

输出结果如下

throw 向生成器内部抛出一个异常

def gen_func():
    
    yield "http://projectsedu.com"
    yield 2
    yield 3
    return "bobby"

if __name__ == "__main__":
    gen = gen_func()
    print(next(gen))
    gen.throw(Exception, "download error")

 

输出结果如下

 

 yield 和 yield from 区别

yield 返回的是一个具体的值

from itertools import chain

my_list = [1,2,3]

def my_chain(*args, **kwargs):
    print(args)
    for my_iterable in args:

        for value in my_iterable:
            yield value

for value in my_chain(my_list, range(5,10)):
    print(value)

 

输出结果如下

 

yield from 返回的是一个可迭代的对象,在内部帮助我们遍历取值

my_list = [1,2,3]

def my_chain(*args, **kwargs):
    print(args)
    for my_iterable in args:

        yield from my_iterable

for value in my_chain(my_list, range(5,10)):
    print(value)

  

 输出结果如下

 

生成器进阶-yield from

yield from 在内部帮助我们做了大量的异常处理

 一段伪代码如下

def g1(gen):
    yield from gen

def main():
    g = g1()
    g.send(None)

1  main 调用方 g1 (委托生成器)  gen 子生成器
2  yield from 会在调用方 main 与子生成器 gen 之间建立一个双向通道

3 在这个双向通道内 调用方main可以直接传值给子生成器gen 

4  yield from 会把子生成器的最终的值返回给委托生成器 

 案例:统计一个字典中value为列表对其求和返回

main调用方调用委托生成器middle 通过yield from 和子生成器 sales_sum 建立双向通道

通过双向通道向其传值,通过send(none) 编程停止,子生成器通过yield from 把最终的统计结果返回给委托生成器

 

#python3.3新加了yield from语法
final_result = {}


def sales_sum(pro_name):
    total = 0
    nums = []
    while True:
        x = yield
        print(pro_name+"销量: ", x)
        if not x:
            break
        total += x
        nums.append(x)
    return total, nums

def middle(key):
    while True:
        final_result[key] = yield from sales_sum(key)
        print(key+"销量统计完成!!.")

def main():
    data_sets = {
        "bobby牌面膜": [1200, 1500, 3000],
        "bobby牌手机": [28,55,98,108 ],
        "bobby牌大衣": [280,560,778,70],
    }
    for key, data_set in data_sets.items():
        print("start key:", key)
        m = middle(key)  # 委托生成器
        m.send(None) # 预激middle协程
        for value in data_set:
            m.send(value)   # 给协程传递每一组的值
        m.send(None)
    print("final_result:", final_result)

if __name__ == '__main__':
    main()

 

输出结果如下

async和await

 python为了将语义变得更加明确,因为有时候我们通过yield 关键字很难区分一个函数是生成器还是协程,就引入了async和await关键词用于定义原生的协程

import types

@types.coroutine
def downloader(url):
    yield "zhangbiao"

async def download_url(url):
    html = await downloader(url)
    return html

if __name__ == "__main__":
    coro = download_url("http://www.imooc.com")
    res = coro.send(None)
    print(res)

  

 输出结果如下

 

 

 

 

posted @ 2018-12-11 23:15  Crazymagic  阅读(360)  评论(0编辑  收藏  举报