Python高级编程和异步IO并发编程


 一、协程和异步io

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

  • 并发: 是指一个时间段内,有几个程序在同一个cpu上运行,但是任意时刻只有一个程序在cpu上运行。
  • 并行: 是指任意时刻点上,有多个程序同时运行在多个cpu上。
  • 同步: 是指代码调用IO操作时,必须等待IO操作完成才返回的调用方式。
  • 异步: 是指代码调用IO操作时,不必等待IO操作完成就返回的调用方式。
  • 阻塞: 是指调用函数时候当前线程被挂起。
  • 非阻塞: 是指调用函数时候当前线程不会被挂起,而是立即返回。

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

Unix下五种 I/O 模型:

  • 阻塞式 I/O
  • 非阻塞 I/O
  • I/O 复用
  • 信号驱动式 I/O
  • 异步 I/O (POSIX的 aio_ 系列函数)


 

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

 ** urllib.parse下的urlparse解析:

from urllib.parse import urlparse :用于解析url,将url分离开 ↓
# from urllib.parse import urlparse

url = "http://www.baidu.com/test.py?a=hello&b=world "
url = urlparse(url)
# 结果:
ParseResult(scheme='http', netloc='www.baidu.com', path='/test.py', params='', query='a=hello&b=world ', fragment='')
# url.netloc = 'www.baidu.com'
# url.path = '/test.py'

 1)适用于linux  epoll(未完善,问题:无法调用 EVENT_READ)

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

selector = DefaultSelector() # 自动选择,win下选select ,linux下选epoll
class Fetcher(object):
# 模拟客户端(浏览器)向服务端请求url数据(即HTTP请求)
# select + 回调函数 + 事件循环 ,经典模式


    def readable(self, key):
        # 回调函数,接收服务器返回数据
        print("1111111")
        d = self.client.recv(1024)  # 不需要使用while True,只要有数据可读,自动会不断调用
        if d:
            self.data += d
        else:
            selector.unregister(key.fd)  # 注销掉该文件描述符(ready)
            data = self.data.decode("utf8")
            html_data = data.split("\r\n\r\n")[1]  # html内容
            print(html_data)
            self.client.close()

    def connected(self,key):
        # 回调函数,向服务器发送数据
        selector.unregister(key.fd) # 注销掉该文件描述符(write/ready),此为write ,每次成功连接后都应该注销  (key:SelectorKey对象)
        # self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(self.path, self.host).encode("utf8"))# 发送请求头,编码成字节格式发送
        self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\n\r\n".format(self.path, self.host).encode("utf8"))# 发送请求头,编码成字节格式发送

        selector.register(self.client.fileno(), events=EVENT_READ, data=self.readable)  # 注册   当可写时,说明服务器已经接收到我们的信息并给我们返回响应信息

    def get_url(self,url):
        # url拆分
        url = urlparse(url)
        # print(url)
        self.host = url.netloc
        self.path = url.path
        self.data = b""
        if self.path == "":
            self.path = '/'

        # 建立连接
        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

        # 注册  可写时说明已经连接成功,可以向服务器发送数据
        selector.register(self.client.fileno(),EVENT_WRITE,self.connected)


def loop():
    # 事件循环,不停的请求socket的状态并调用对应的回调函数
    # 1. select本身是不支持register模式
    # 2. socket状态变化以后的回调是由程序员完成的
    ready = selector.select()  # 不断请求socket状态
    for key , mask in ready:   # 当状态是我们需要的,调用相应的回调函数。注意:回调函数实际是在这里调用的,而不是在注册时调用
        call_back = key.data  # 回调函数,key是SelectorKey对象,里面的data即是回调函数
        call_back(key)  # 执行回调函数 ,key:SelectorKey对象

if __name__ == '__main__':
    import time
    start_time = time.time()
    for i in range(6):
        # url = "http://101.200.45.75:9000/course/detail/{}/".format(i+1)
        url = "http://www.baidu.com"
        # url = "http://shop.projectsedu.com/goods/{}/".format(i)

        fetcher = Fetcher()
        fetcher.get_url(url)
    loop() # 类似select IO多路复用,不断查询socket状态
    print(time.time() - start_time)
select - HTTP请求

2)适用于win  select

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

selector = DefaultSelector() # 自动选择,win下选select ,linux下选epoll
urls = []  # 用于判断爬虫是否全部爬完数据
stop = False


class Fetcher(object):
# 模拟客户端(浏览器)向服务端请求url数据(即HTTP请求)
# select + 回调函数 + 事件循环 ,经典模式


    def readable(self, key):
        # 回调函数,接收服务器返回数据

        d = self.client.recv(1024)  # 不需要使用while True,只要有数据可读,自动会不断调用
        if d:
            self.data += d
        else:
            selector.unregister(key.fd)  # 注销掉该文件描述符(ready)
            data = self.data.decode("utf8")
            html_data = data.split("\r\n\r\n")[1]  # html内容
            print(html_data)
            self.client.close()
            urls.remove(self.spider_url)
            if not urls: # 全部爬虫都完成数据爬取,准备关掉程序
                global stop
                stop = True

    def connected(self,key):
        # 回调函数,向服务器发送数据
        selector.unregister(key.fd) # 注销掉该文件描述符(write/ready),此为write ,每次成功连接后都应该注销  (key:SelectorKey对象)
        self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(self.path, self.host).encode("utf8"))# 发送请求头,编码成字节格式发送
        # self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\n\r\n".format(self.path, self.host).encode("utf8"))# 发送请求头,编码成字节格式发送

        selector.register(self.client.fileno(), events=EVENT_READ, data=self.readable)  # 注册   当可写时,说明服务器已经接收到我们的信息并给我们返回响应信息

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

        # 建立连接
        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

        # 注册  可写时说明已经连接成功,可以向服务器发送数据
        selector.register(self.client.fileno(),EVENT_WRITE,self.connected)


def loop():
    # 事件循环,不停的请求socket的状态并调用对应的回调函数
    # 1. select本身是不支持register模式
    # 2. socket状态变化以后的回调是由程序员完成的
    while not stop:
        ready = selector.select()  # 不断请求socket状态
        for key , mask in ready:   # 当状态是我们需要的,调用相应的回调函数。注意:回调函数实际是在这里调用的,而不是在注册时调用
            call_back = key.data  # 回调函数,key是SelectorKey对象,里面的data即是回调函数
            call_back(key)  # 执行回调函数 ,key:SelectorKey对象

if __name__ == '__main__':
    import time
    start_time = time.time()
    for i in range(6):
        # url = "http://101.200.45.75:9000/course/detail/{}/".format(i+1)
        url = "http://www.baidu.com"
        # url = "http://shop.projectsedu.com/goods/{}/".format(i)
        urls.append(url) # 用于判断爬虫是否爬取完毕

        fetcher = Fetcher()
        fetcher.get_url(url)
    loop() # 类似select IO多路复用,不断查询socket状态
    print(time.time() - start_time)
select - HTTP请求

解析:

  • selector = DefaultSelector():自动选择IO多路复用机制 , win下选择select ,linux下选择epoll
  • selector.register(self.client.fileno(),EVENT_WRITE,self.connected):注册socket,当数据可写时调用,执行后面的回调函数(self.connected)
  • selector.register(self.client.fileno(), events=EVENT_READ, data=self.readable):注册socket,当数据可读时调用,执行后面的回调函数(self.connected)
  • selector.unregister(key.fd) # 注销掉该文件描述符(write/ready)
  • loop() # 类似select IO多路复用,不断查询socket状态

 

使用上面的方法来实现IO多路复用,虽然比以往传统型同步操作 性能要高的多,但这种IO多路复用的方式也有缺点:

  • 可读性差 
  • 共享状态管理困难 
  • 异常处理困难

 


 

4、协程是什么

  • C10M问题:

    • 如何利用8核cpu、64G内存,在10gbps的网络上保持1000万并发连接。
  • 高并发的问题:

    • 1)回调模式编码复杂度高                   # IO多路复用
    • 2)同步编程的并发性不高                   # 同步编码
    • 3)多线程编程需要线程间同步,lock    # 多线程编码
  • 解决方法:

    • 1)采用同步的方式去编写异步的代码
    • 2)使用单线程去切换任务:
      • 线程是由操作系统切换的,单线程切换意味着我们需要程序员自己去调度任务
      • 不再需要锁,并发性高,如果单线程内切换函数,性能远高于线程切换,并发性更高
  • 传统函数调用过程 A -> B -> C 。我们需要一个可以暂停的函数,并且可以在适当的时候恢复该函数的继续执行。此时出现了协程 -> 有多个入口的函数,可以暂停的函数(可以向暂停的地方传入值

协程概念理解:有多个入口的函数 , 或称 可以暂停的函数(同时可以向暂停的地方传入值)

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

1)send()方法 

url = gen.send(None) :生成器首次调用时,如果使用send方法,参数必须为 None  

.send():传递值到生成器内部,同时重启生成器执行到下一个yield位置

next(gen) : 一个个取值(生成器)

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

if __name__ == "__main__":
    gen = gen_func()  # 生成生成器对象
    #在调用send发送非none值之前,我们必须启动一次生成器, 方式有两种1. gen.send(None), 2. next(gen)
    url = gen.send(None)
    # url = next(gen)

    html = "baby"
    print(gen.send(html)) #send方法可以传递值进入生成器内部,同时还可以重启生成器执行到下一个yield位置
    print(gen.send(html))

 

 

2) 生成器的close方法:

 生成器对象的close方法会在生成器对象方法的挂起处抛出一个GeneratorExit异常。GeneratorExit异常产生后,系统会继续把生成器对象方法后续的代码执行完毕。参见下面的代码:

def myGenerator(): 
     try:
        yield 1 
        print "Statement after yield"
     except GeneratorExit:
        print "Generator error caught"
     print "End of myGenerator" 

gen = myGenerator()
print gen.next()
gen.close()
print "End of main caller"

# GeneratorExit继承于BaseException, 不是Exception

代码执行过程如下:

  • 当调用gen.next方法时,会激活生成器,直至遇到生成器方法的yield语句,返回值1。同时,生成器方法的执行被挂起。
  • 当调用gen,close方法时,恢复生成器方法的执行过程。系统在yield语句处抛出GeneratorExit异常,执行过程跳到except语句块。当except语句块处理完毕后,系统会继续往下执行,直至生成器方法执行结束。

执行结果:

1 1
2 Generator error caught
3 End of myGenerator
4 End of main caller

 

 需要注意的是,GeneratorExit异常的产生意味着生成器对象的生命周期已经结束。因此,一旦产生了GeneratorExit异常,生成器方法后续执行的语句中,不能再有yield语句,否则会产生RuntimeError,同时 RuntimeError会向主方法抛出异常,后面的代码的将不会再被执行

 3)生成器的throw方法:

def gen_func():
    try:
        yield "http://projectsedu.com"
    except Exception as e:
        pass
    yield 2
    yield 3
    return "MJ"

if __name__ == "__main__":
    gen = gen_func()
    print(next(gen))  #代码执行到第一个yield
    print(gen.throw(Exception, "download error"))  # 代码执行到第二个yield   ,手动抛异常
    print(next(gen))   # 代码执行到第三个yield ,停止
    gen.throw(Exception, "download error")  # 再抛异常,没有捕捉

 

运行结果:

1 http://projectsedu.com
2 Traceback (most recent call last):
3 2
4 3
5   File "G:/Python/python_sample/test.py", line 15, in <module>
6     gen.throw(Exception, "download error")
7   File "G:/Python/python_sample/test.py", line 7, in gen_func
8     yield 3
9 Exception: download error

 

6、生成器进阶-yield from 

yield from  与  yield 不同点:

yield:给到什么值,返回就是什么值,比如:yield range(10) , 返回结果就是:range(0,10)

yield from :会将给到的数据一个个返回,比如:yield from range(10) ,返回结果是:0 ,1 , 2 ,3 ,4 ,5, ,6 ,7 ,8 ,9 ,10 

 

 将有yield from的生成器当作委托生成器 ,yield from(委托生成器)会在调用方与子生成器之间建立一个双向通道,yield from作用解析:

假设 A 函数中有这样一个语句:

 yield from B() 

B() 返回的是一个可迭代(iterable)的对象b,那么A()会返回一个 generator——照我们的命名规范,名字叫a——那么:

  b迭代产生的每个值都直接传递给a的调用者。 
 所有通过send方法发送到a的值都被直接传递给b. 如果发送的 值是None,则调用b的__next__()方法,否则调用b的send 方法。如果对b的方法调用产生StopIteration异常,a会继续 执行yield from后面的语句,而其他异常则会传播到a中,导 致a在执行yield from的时候抛出异常。 
 如果有除GeneratorExit以外的异常被throw到a中的话,该异常 会被直接throw到b中。如果b的throw方法抛出StopIteration, a会继续执行;其他异常则会导致a也抛出异常。 
 如果一个GeneratorExit异常被throw到a中,或者a的close 方法被调用了,并且b也有close方法的话,b的close方法也 会被调用。如果b的这个方法抛出了异常,则会导致a也抛出异常。 反之,如果b成功close掉了,a也会抛出异常,但是是特定的  GeneratorExit异常。 
 a中yield from表达式的求值结果是b迭代结束时抛出的  StopIteration异常的第一个参数。 
 b中的return 语句实际上会抛出StopIteration( ) 异常,所以b中return的值会成为a中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
            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异常。

"""
yield 说明

关键点:

  • 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异常

 

 demo:

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  # (5700, [1200, 1500, 3000])   --total:5700 ; nums:[1200, 1500, 3000]

def middle(key):
    while True:
        final_result[key] = yield from sales_sum(key)  # 子生成器抛出异常StopIteration,会将return中的值:total, nums, 传回给 final_result
        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协程 ,子生成器中执行next()方法
        for value in data_set:  # 委托生成器运行中止,以下都是调用方与子生成器的直接对接,直到数据yield完之后,抛出StopIteration异常,委托生成器拿回主控器,继续运行下面的代码,同时子生成器中return的值会传给委托生成器
            m.send(value)   # 调用方调用send方法,直接传到子生成器调用子生成器send()方法
        m.send(None)  # 手动抛出StopIteration异常,退出调用方与子生成器的直接对接,让委托生成器恢复运行后续代码
    print("final_result:", final_result) # final_result:final_result: {'bobby牌面膜': (5700, [1200, 1500, 3000]), 'bobby牌手机': (289, [28, 55, 98, 108]), 'bobby牌大衣': (1688, [280, 560, 778, 70])}

if __name__ == '__main__':
    main()
yield from

 

输出结果:

start key: bobby牌面膜
bobby牌面膜销量:  1200
bobby牌面膜销量:  1500
bobby牌面膜销量:  3000
bobby牌面膜销量:  None
bobby牌面膜销量统计完成!!.
start key: bobby牌手机
bobby牌手机销量:  28
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])}

Process finished with exit code 0
输出结果

 

 7、async和await

#python为了将语义变得更加明确,就引入了async和await关键词,用于定义原生的协程

async def downloader(url):
    return "hello"


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

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


 

 

posted on 2018-10-11 02:12  Eric_nan  阅读(294)  评论(0编辑  收藏  举报