菜鸟初学python入门进阶第十一节:面向对象,协程和异步io

这篇是嗯货


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

并发:一个时间段内,有几个程序在同一个cpu上运行,但任意时刻只有一个程序在cpu上运行
并行:任意时刻点上,有多个程序同时运行在多个cpu上
同步:指代码调用io操作时,必须等待io操作完成才返回的调用方式
异步:指代码调用io操作时,不必等待io操作完成才返回的调用方式

多线程是典型异步操作
io操作可以看成是消息通信

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

阻塞非阻塞是函数调用机制

2.unix下5中i/o模型

来自《unix网络编程》
在这里插入图片描述
阻塞式io在等待时间里会浪费大量cpu
在这里插入图片描述
在建立socket之后加一句client.setblocking(False)
不会等待连接的建立,但如果没有建立完连接就send了信息,会抛异常
所以需要while循环去检查连接状态
但是while循环也要耗费cpu

可以用别的操作拖时间
在这里插入图片描述
i/o多路复用的select可以返回哪些连接已经ok了,select是阻塞的,但可以同时监听多个socket
在这里插入图片描述

在这里插入图片描述

3.C10K问题和io多路复用

如何在1颗1GHz的cpu,2G内存,1gbps网络环境下,让单台服务器同时为10000个客户端提供FTP服务

一个线程只能处理一个socket,即一个用户
线程能开的不多

如何在简单服务器上处理多个用户呢?
需要io多路复用

select、poll、epoll 都是io多路复用的机制
本质都是同步io,读写过程是阻塞的

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在并发高的情况下,且连接活跃度不高(不会太调用连接),epoll比select好
在并发不高,连接很活跃的情况下,selecct比epoll好

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

先使用非阻塞io

import socket
from urllib.parse import urlparse
'''parse用来解析url'''

def get_url(url):
    '''通过socket请求url'''
    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))
    except BlockingIOError as e:
        pass

    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')

使用select

import socket
from urllib.parse import urlparse
'''parse用来解析url'''
import select # 用的不多
from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
# 包装得更好,不用考虑select或epoll,还提供注册机制


selector = DefaultSelector()

urls = ["http://www.baidu.com"]

stop = False

class Fetcher:
    def get_url(self, url):
        self.spider_url = url
        '''通过socket请求url'''
        url = urlparse(url)
        self.host = url.netloc
        self.path = url.path
        if self.path == '':
            self.path = '/'
        '''注意要这样写,当readable时,不代表可以不停从内核空间拷贝数据到应用空间,可能会分次拷到data中,所以声明一个实例属性比较好'''
        self.data = b""


        '''建立socket连接'''
        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.client.setblocking(False)
        try:
            self.client.connect((self.host, 80))
        except BlockingIOError as e:
            pass

        # socket注册到selector之中
        selector.register(self.client.fileno(), EVENT_WRITE, self.connected)

    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 loop():
    # 事件循环,不停请求socket状态并调用回调函数
    # select本身不支持register模式
    # socket状态变化以后的回调是由程序员完成的
    while not stop:
        ready = selector.select() # windows下会抛出一个异常,需要改一下,这里已经解决
        for key, mask in ready:
            call_back = key.data
            call_back(key)


if __name__ == '__main__':
    fetcher = Fetcher()
    fetcher.get_url("http://www.baidu.com")
    loop()

5.使用事件循环+回调不好的地方

回调函数有异常比较难处理
回调如果嵌套太多会难维护
如果某个环节有问题会比较麻烦
变量可能难以维护
变量共享困难

6.协程

C10M问题:如何利用8核cpu,64g内存,在10gbps的网络上保持1000w并发
由于:
回调模式的编码复杂性高;
同步编程的并发性不高;
多线程需要线程间同步,要用lock;

所以采取:
同步方式编写异步代码;
使用单线程去切换任务;

线程是由操作系统切换的,单线程切换意味着我们需要自己调度任务,但好处是不用锁了,并发性高
单线程内切换函数,性能远高于线程间切换

我们需要一个可以暂停的函数,并在适当的时候恢复该函数的继续执行
协程---->有多个入口的函数,可以暂停的函数,可以向暂停的地方传入值

传统函数是调用栈的,像递归
我们不是像传统函数一样在内部有新函数时入栈,而是抛出一个任务,抛到另一个函数中,另一个函数执行完了,再把值传回来

python的生成器是可以暂停的

7.生成器进阶-send、close和throw

一般生成器有

def get_func():
    yield 1
    yield 2
    yield 3
    return "okkk"

if __name__ == '__main__':
    '''获取生成器'''
    gen = get_func()
    '''想要启动生成器,可以使用next(),生成器实现了迭代协议'''
    print(next(gen))
    print(next(gen))
    print(next(gen))
    print(next(gen))
Traceback (most recent call last):
  File "D:/temp/temp.py", line 15, in <module>
    print(next(gen))
StopIteration: okkk
1
2
3

每yield一次就在栈帧处打个标记,每启动一次生成器就会回到打标记的地方,按照打标记的链顺序执行完到下一处标记(yield)处,最后return处会抛出一个stopiteration 并返回return的值

生成器不只可以产出值,还可以接收值
接收值是调用生成器方传入的值

def get_func():
    html = yield "nothing"
    print(html)
    yield 2
    yield 3
    return "okkk"

if __name__ == '__main__':
    gen = get_func()
    '''想要启动生成器,可以使用next(),生成器实现了迭代协议'''
    '''先运行函数,到第一个yield'''
    '''不要直接就 url = gen.send(某个具体值),因为函数还没运行'''
    '''只能 url = gen.send(None)'''
    '''在调用send发送非none值之前,必须先启动一次生成器'''
    url = next(gen)
    '''send方法可以传递值进入生成器内部,同时还可以重启生成器执行到下一个yield的位置,
    并执行完下一个yield
    '''
    html = "pixiv"
    gen.send(html)
pixiv

关于close()

gen.close()会在当前运行完的yield处抛出一个异常
可以用try-except 抓获
close是关闭生成器了,如果之后还有yield,会抛异常
如果只剩下return,不会抛异常,单纯 StopIteration
注意一个区别:

def get_func():
    try:
    	yield 1
    except GeneratorExit:
    	pass
    yield 2
    yield 3
    return "okkk"

if __name__ == '__main__':
    '''获取生成器'''
    gen = get_func()
    '''想要启动生成器,可以使用next(),生成器实现了迭代协议'''
    print(next(gen))
    gen.close()
Traceback (most recent call last):
  File "D:/temp/temp.py", line 15, in <module>
    gen.close()
RuntimeError: generator ignored GeneratorExit
1

如果直接pass,其异常会向上抛,抛到close(),也就是主函数会中断
不pass的话:

def get_func():
    # try:
    yield 1
    # except GeneratorExit:
    #     pass
    yield 2
    yield 3
    return "okkk"

if __name__ == '__main__':
    '''获取生成器'''
    gen = get_func()
    '''想要启动生成器,可以使用next(),生成器实现了迭代协议'''
    print(next(gen))
    gen.close()
    print("hi")
1
hi

主函数不会抛异常
但如果抓异常时候是写except Exception和 pass 的话,也不会抛异常
原因是GeneratorExit继承的是BaseException而不是Exception,base的更基类

关于throw

throw会在当前运行完的yield处抛出一个异常

def get_func():
    yield 1
    print("hii")
    yield 2
    yield 3
    return "okkk"

if __name__ == '__main__':
    gen = get_func()
    print(next(gen))
    gen.throw(Exception, "shit")
Traceback (most recent call last):
  File "D:/temp/temp.py", line 11, in <module>
    gen.throw(Exception, "shit")
  File "D:/temp/temp.py", line 2, in get_func
    yield 1
Exception: shit
1

Process finished with exit code 1

8.yield-from

python3.3新加特性

from itertools import chain
'''迭代工具'''

my_list = [1,2,3]
my_dict = {
    'a':'aa',
    'b':'bb',
}

for value in chain(my_list, my_dict, range(7, 9)):
    print(value)
1
2
3
a
b
7
8

自己实现chain

def my_chain(*args, **kwargs):
    for my_iter in args:
        for value in my_iter:
            yield value

也可以

def my_chain(*args, **kwargs):
    for my_iter in args:
        yield from my_iter

yield from 可以把可迭代对象里面的具体值一个个拿出来

再看一段代码

def g1(gen):
    yield from gen

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

这里有几个概念
main 是调用方,g1 是委托生成器,gen 是子生成器
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有两层可迭代对象'''
    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()

输出效果:

start key: bobby牌面膜
bobby牌面膜销量:  1200
bobby牌面膜销量:  1500
bobby牌面膜销量:  3000
bobby牌面膜销量:  None
bobby牌面膜销量统计完成!!.
bobby牌手机销量:  55
bobby牌手机销量:  98
bobby牌手机销量:  108
bobby牌手机销量:  None
bobby牌手机销量统计完成!!.
start key: bobby牌大衣
bobby牌大衣销量:  280
bobby牌大衣销量:  560
bobby牌大衣销量:  778
bobby牌大衣销量:  70
bobby牌大衣销量:  None
bobby牌大衣销量统计完成!!.
final_result: {'bobby牌面膜': (5700, [1200, 1500, 3000]), 'bobby牌手机': (289, [28, 55, 98, 108]), 'bobby牌大衣': (1688, [280, 560, 778, 70])}

感觉上是可以把子生成器的逻辑放到委托生成器里的,为什么还要委托生成器?
yield from 还处理了很多异常抛出
根据上面几篇对yield的分析,我们如果不写yield from ,就必须亲自处理stopiteration的异常抛出
不仅如此
看看yield from的文档

#pep380

#1. RESULT = yield from EXPR可以简化成下面这样
#一些说明
"""
_i:子生成器,同时也是一个迭代器
_y:子生成器生产的值
_r:yield from 表达式最终的值
_s:调用方通过send()发送的值
_e:异常对象

"""

_i = iter(EXPR)      # EXPR是一个可迭代对象,_i其实是子生成器;
try:
    _y = next(_i)   # 预激子生成器,把产出的第一个值存在_y中;
except StopIteration as _e:
    _r = _e.value   # 如果抛出了`StopIteration`异常,那么就将异常对象的`value`属性保存到_r,这是最简单的情况的返回值;
else:
    while 1:    # 尝试执行这个循环,委托生成器会阻塞;
        _s = yield _y   # 生产子生成器的值,等待调用方`send()`值,发送过来的值将保存在_s中;
        try:
            _y = _i.send(_s)    # 转发_s,并且尝试向下执行;
        except StopIteration as _e:
            _r = _e.value       # 如果子生成器抛出异常,那么就获取异常对象的`value`属性存到_r,退出循环,恢复委托生成器的运行;
            break
RESULT = _r     # _r就是整个yield from表达式返回的值。

"""
上面是简化的版本,有一些问题
1. 子生成器可能只是一个迭代器,并不是一个作为协程的生成器,所以它不支持.throw()和.close()方法;
2. 如果子生成器支持.throw()和.close()方法,但是在子生成器内部,这两个方法都会抛出异常;
3. 调用方让子生成器自己抛出异常
4. 当调用方使用next()或者.send(None)时,都要在子生成器上调用next()函数,
   当调用方使用.send()发送非 None 值时,才调用子生成器的.send()方法;
"""
_i = iter(EXPR)
try:
    _y = next(_i)
except StopIteration as _e:
    _r = _e.value
else:
    while 1:
        try:
            _s = yield _y
        except GeneratorExit as _e:
            try:
                _m = _i.close # 子生成器可能只是一个迭代器,可能没有close方法
            except AttributeError:
                pass
            else:
                _m() # 如果有就尝试调用
            raise _e # 最后还是抛出异常
        except BaseException as _e:
            _x = sys.exc_info()
            try:
                _m = _i.throw
            except AttributeError:
                raise _e
            else:
                try:
                    _y = _m(*_x)
                except StopIteration as _e:
                    _r = _e.value
                    break
        else:
            try:
                if _s is None:
                    _y = next(_i)
                else:
                    _y = _i.send(_s)
            except StopIteration as _e:
                _r = _e.value
                break
RESULT = _r

"""
看完代码,我们总结一下关键点:

1. 子生成器生产的值,都是直接传给调用方的;
   调用方通过.send()发送的值都是直接传递给子生成器的;
   如果发送的是 None,会调用子生成器的__next__()方法,如果不是 None,会调用子生成器的.send()方法;
2. 子生成器退出的时候,最后的return EXPR,会触发一个StopIteration(EXPR)异常;
3. yield from表达式的值,是子生成器终止时,传递给StopIteration异常的第一个参数;
4. 如果调用的时候出现StopIteration异常,委托生成器会恢复运行,同时其他的异常会向上 "冒泡";
5. 传入委托生成器的异常里,除了GeneratorExit之外,其他的所有异常全部传递给子生成器的.throw()方法;
   如果调用.throw()的时候出现了StopIteration异常,那么就恢复委托生成器的运行,其他的异常全部向上 "冒泡";
6. 如果在委托生成器上调用.close()或传入GeneratorExit异常,会调用子生成器的.close()方法,没有的话就不调用。
   如果在调用.close()的时候抛出了异常,那么就向上 "冒泡",
   否则的话委托生成器会抛出GeneratorExit异常。

"""

9.python原生的协程async和await

python3.5后有了原生的协程
目的是明确语义
因为有时候会搞混生成器与协程

async 里不能写yield,只能写await

可以将await理解成yield from

# async def downloader(url):
#     return "bobby"
# 或者
import types
@types.coroutine
def downloader(url):
    yield "bobby"

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

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

10.事件循环+协程

协程是程序员自己调度的,线程是操作系统调度的

'''生成器的状态是可以获取的'''
import inspect
def gen_func():
    yield 1
    return 'shit'

if __name__ == '__main__':
    gen = gen_func()
    print(inspect.getgeneratorstate(gen))
    next(gen)
    print(inspect.getgeneratorstate(gen))
    try:
        next(gen)
    except StopIteration:
        pass
    print(inspect.getgeneratorstate(gen))
GEN_CREATED
GEN_SUSPENDED
GEN_CLOSED

上面的gen_func已经可以叫做协程了
协程与生成器不同之处在于可以消费外边传进来的数据,不单单产出

之前已经说明了事件循环+回调模式不好的地方,回调层数多了可能会逻辑混乱,且变量维护困难

协程因为是暂停函数,不是回调,逻辑会相对清晰,接近于同步的方式编写代码

尽管事件循环+协程模式不一定比事件循环+回调块,但更方便

通过事件循环知道哪一个协程连接ok了,再启动那个协程

协程是单线程模式

下一篇详细搞协程

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值