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


 一、asyncio并发编程

1、事件循环

  事件循环+回调(驱动生成器)+epoll(IO多路复用)

   asyncio 是 python 用于解决异步IO编程的一整套解决方案

asyncio

  • 包含各种特定系统实现的模块化事件循环
  • 传输和协议抽象
  • 对 TCP、UDP、SSL、子进程、延时调用以及其他的具体支持
  • 模仿 futures 模块但适用于事件循环使用的 Future 类
  • 基于 yield from 的协议和任务,可以让你用顺序的方式编写并发代码
  • 必须使用一个将产生阻塞 IO 的调用时,有接口可以把这个事件转移到线程池
  • 模仿 threading 模块中的同步原语、可以用在单线程内的协程之间

asyncio的几个概念:

  • event_loop 事件循环:程序开启一个无限的循环,程序员会把一些函数注册到事件循环上。当满足事件发生的时候,调用相应的协程函数。
  • coroutine 协程:协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用。
  • task 任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含任务的各种状态。
  • future: 代表将来执行或没有执行的任务的结果。它和task上没有本质的区别
  • async/await 关键字:python3.5 用于定义协程的关键字,async定义一个协程,await用于挂起阻塞的异步调用接口。

asyncio常用方法:loop = syncio.get_event_loop()

  • syncio.get_event_loop():创建一个事件循环
  • loop.run_until_complete(coroutine):将协程或者task注册到事件循环并启动事件循环
  • asyncio.ensure_future(coroutine)  和  loop.create_task(coroutine) :都可以创建一个task
  1. 主线程中调用asyncio.get_event_loop()时创建事件循环

  2. 其他线程中调用asyncio.get_event_loop()运行时引发错误

  3. 任何地方可以调用asyncio.set_event_loop()绑定一个事件循环到当前线程。asyncio.new_event_loop() 函数可以创建这个事件循环。事件循环没有绑定到当前线程也可以使用。

  4. asyncio.get_event_loop()返回线程绑定的事件循环,不是返回当前运行的事件循环。

1.1、定义一个协程

定义一个协程很简单,使用async关键字,就像定义普通函数一样:

async def do_some_work(x):
    print('Waiting: ', x)

 通过async关键字定义一个协程(coroutine),协程也是一种对象。协程不能直接运行,需要把协程加入到事件循环(loop),由后者在适当的时候调用协程。asyncio.get_event_loop方法可以创建一个事件循环,然后使用run_until_complete将协程注册到事件循环,并启动事件循环  

 

1.2、创建一个 task

 协程对象不能直接运行,在注册事件循环的时候,其实是run_until_complete方法将协程包装成为了一个任务(task)对象。所谓task对象是Future类的子类。保存了协程运行后的状态,用于未来获取协程的结果

import asyncio
 
async def do_some_work(x):
    print('Waiting: ', x)
 
coroutine = do_some_work(2) # 协程预启动
loop = asyncio.get_event_loop()  # 创建事件循环
# task = asyncio.ensure_future(coroutine)
task = loop.create_task(coroutine)  # 创建一个task
print(task)
loop.run_until_complete(task)  # 注册task并开启事件循环
print(task)

 asyncio.ensure_future(coroutine) 和 loop.create_task(coroutine)都可以创建一个task(future),run_until_complete的参数是一个future对象。当传入的是一个协程,而不是task(future),其内部会自动封装成task,task是Future的子类。上面两个方法都会返回future对象,我们可以通过future对象的result来查看task的执行结果,如:

 get_future = asyncio.ensure_future(coroutine)   

 print(get_future.result()) # 获取task执行结果,即协程中的return值

 

1.3、绑定回调

 绑定回调,在task执行完毕的时候可以获取执行的结果,回调的最后一个参数是future对象,通过该对象可以获取协程返回值。如果回调需要多个参数,可以通过偏函数导入

import time
import asyncio
 
now = lambda : time.time()
 
async def do_some_work(x):
    print('Waiting: ', x)
    return 'Done after {}s'.format(x)
 
def callback(future):  # 回调函数
    print('Callback: ', future.result())
 
start = now()
 
coroutine = do_some_work(2)
loop = asyncio.get_event_loop()
get_future = asyncio.ensure_future(coroutine)
task.add_done_callback(callback)  # 添加回调函数
loop.run_until_complete(get_future)
 
print('TIME: ', now() - start)

 回调函数需要多个参数时,future参数要放最后。执行完成,我们可以通过参数future获取协程的执行结果:future.result()

import functools   # functools.partial:偏函数,能将带参数的函数包装成一个新的函数
def
callback(t, future): # 回调函数 ,future放最后 print('Callback:', t, future.result()) task.add_done_callback(functools.partial(callback, 2)

1.4、future 、 result

 回调一直是很多异步编程的恶梦,程序员更喜欢使用同步的编写方式写异步代码,以避免回调的恶梦。回调中我们使用了future对象的result方法。

 在不使用回调函数的情况下,我们可以看到task有fiinished状态。当task finished时,可以直接读取task的result方法获取协程执行结果

async def do_some_work(x):
    print('Waiting {}'.format(x))
    return 'Done after {}s'.format(x)
 
start = now()
 
coroutine = do_some_work(2)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(coroutine)
loop.run_until_complete(task)
 
print('Task ret: {}'.format(task.result()))
print('TIME: {}'.format(now() - start))

 

输出结果:

Waiting:  2
Task ret:  Done after 2s
TIME:  0.0003650188446044922

 

1.5、阻塞和 await

 使用async可以定义协程对象,使用await可以针对耗时的操作进行挂起,就像生成器里的yield一样,函数让出控制权。协程遇到await,事件循环将会挂起该协程,执行别的协程,直到其他的协程也挂起或者执行完毕,再进行下一个协程的执行。

 耗时的操作一般是一些IO操作,例如网络请求,文件读取等。我们使用asyncio.sleep函数来模拟IO操作。协程的目的也是让这些IO操作异步化。

 在 sleep的时候,使用await让出控制权。即当遇到阻塞调用的函数的时候,使用await方法将协程的控制权让出,以便loop调用其他的协程。现在我们的例子就用耗时的阻塞操作了

 

1.6、wait 和 gather

wait :是个协程函数,在run_until_complete() 中使用:

# 等待所有任务执行完成
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))  # tasks是个列表,里面放有多个url,即执行多个任务

 

gather(推荐使用):抽象性比wait更高一层,可以分组完成,也可以批量结束任务。执行多任务时同wait,但输出会有所差异

# 等待所有任务执行完成
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(*tasks))  # *号必须要有

 

gather解说:

group1 = [get_html("http://projectsedu.com") for i in range(2)]  # 分组
    group2 = [get_html("http://www.imooc.com") for i in range(2)]  # 分组

    # group1 = asyncio.gather(*group1)
    # group2 = asyncio.gather(*group2)
    # loop.run_until_complete(asyncio.gather(group1, group2))

    # group2.cancel() # 只结束一组任务
    loop.run_until_complete(asyncio.gather(group1, group2)) # 分组执行,分组完成

 


 

2、task取消和子协程调用原理

2.1、run_forever 与 run_until_complete 区别

loop = asyncio.get_event_loop()

loop.run_forever() :一直执行,不会停止,直到调用stop()方法才会停止

loop.run_until_complete() :一直运行,当future任务执行完成,停止运行

2.2、协程停止(task取消)

future对象有几个状态:

  • Pending
  • Running
  • Done
  • Cancelled

创建future的时候,task为pending,事件循环调用执行的时候当然就是running,调用完毕自然就是done,如果需要停止事件循环,就需要先把task取消。可以使用asyncio.Task获取事件循环的task

get_future = asyncio.ensure_future(coroutine):创建future ,task状态为pending

 asyncio.Task.all_tasks():获取事件循环中的所有task

import asyncio
import time

async def get_html(sleep_times): # 协程任务
    print("waiting")
    await asyncio.sleep(sleep_times)
    print("done after {}s".format(sleep_times))

if __name__ == "__main__":
    task1 = get_html(2)
    task2 = get_html(3)
    task3 = get_html(3)

    tasks = [task1, task2, task3] # 多个task任务

    loop = asyncio.get_event_loop()  # 获取事件循环

    try:
        loop.run_until_complete(asyncio.wait(tasks)) # 注册tasks多个任务,并开启事件循环
    except KeyboardInterrupt as e: # 终端执行,开启事件循环后按ctrl+c结束任务,会报KeyboardInterrupt异常,我们在这里取消task任务
        all_tasks = asyncio.Task.all_tasks()  # 获取事件循环中所有task任务
        for task in all_tasks:
            task.cancel()  # 取消task,有返回值,为True则取消成功,为False则取消失败
        loop.stop()  # 停止事件循环
        loop.run_forever() # 重新开启,必须跟随stop函数一起调用,不然会报错
    finally:
        loop.close() # 关闭

 

2.2、协程嵌套(协程调用原理)

 使用async可以定义协程,协程用于耗时的io操作,我们也可以封装更多的io操作过程,这样就实现了嵌套的协程,即一个协程中await了另外一个协程,如此连接起来

官网实例:

 

图解:

 

原理解析:

 类似之前讲过的生成器:调用方、委托生成器、子生成器。

 1、run_until_complete运行,会注册task(协程:print_sum)并开启事件循环 →

 2、print_sum协程中嵌套了子协程,此时print_sum协程暂停(类似委托生成器),转到子协程(协程:compute)中运行代码,期间子协程需sleep1秒钟,直接将结果反馈到event loop中,即将控制权转回调用方,而中间的print_sum暂停不操作 →

 3、1秒后,调用方将控制权给到子协程(调用方与子协程直接通信),子协程执行接下来的代码,直到再遇到wait(此实例没有)→

 4、 最后执行到return语句,子协程向上级协程(print_sum抛出异常:StopIteration),同时将return返回的值返回给上级协程(print_sum中的result接收值),print_sum继续执行暂时时后续的代码,直到遇到return语句 →

 5、向 event loop 抛出StopIteration异常,此时协程任务都已经执行完毕,事件循环执行完成(event loop :the loop is stopped),close事件循环。


 

3、call_soon、call_at、call_later

1)call_soon :

事件循环时,立即执行指定回调函数:
import asyncio

def callback(sleep_times, loop): # 普通函数
    print("success time {}".format(loop.time()))
def stoploop(loop):  # 停止事件循环
    loop.stop()

if __name__ == "__main__":
    loop = asyncio.get_event_loop() # 获取事件循环
    loop.call_soon(callback, 2) # 事件循环时,立即执行指定回调函数
    loop.call_soon(stoploop, loop) # 停止事件循环,执行完call_soon指定的回调函数后立刻执行
    loop.run_forever() # 普通函数只能用run_forever,run_until_complete是用于协程的。执行run_forever,需手动stop事件循环

2)call_later :延迟调用回调函数。 将上述代码中的call_soon改成call_later:

loop.call_later(3,callback, 2) # 前面的是延迟时间,及3秒后调用callback回调函数

 

3)call_at :在指定的时间点调用,使用的是loop内部的时间,不是我们的实际时间:

import asyncio

def callback(sleep_times, loop):
    print("success time {}".format(loop.time()))
def stoploop(loop):
    loop.stop()

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    now = loop.time() # loop内部时间
    loop.call_at(now+2, callback, 2, loop)
    loop.call_at(now+1, callback, 1, loop)
    loop.call_at(now+3, callback, 3, loop)
    # loop.call_soon(stoploop, loop)
    loop.call_soon(callback, 4, loop)
    loop.run_forever()

 


 

4、ThreadPollExecutor 和 asycio 完成阻塞 IO 请求 

 使用asyncio,尽量不要处理阻塞IO系列,如果一定要处理,将阻塞IO都放到asyncio的线程池运行。注意:使用asyncio线程池跟多线程线程池处理所需时间等是差不多的

demo:

 task = loop.run_in_executor(executor, get_url, url) :集成线程池到asyncio中处理

#使用多线程:在协程中集成阻塞io
import asyncio
from concurrent.futures import ThreadPoolExecutor
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.setblocking(False)
    client.connect((host, 80)) #阻塞不会消耗cpu

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

    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()
    loop = asyncio.get_event_loop() # 获取事件循环
    executor = ThreadPoolExecutor(3)  # 线程池,每次处理三个线程
    tasks = []
    for url in range(20):
        url = "http://shop.projectsedu.com/goods/{}/".format(url) 
        task = loop.run_in_executor(executor, get_url, url)  # 将线程池集成到asyncio中执行。executor:线程池 , get_url:调用的函数 , url:该函数的参数
        tasks.append(task)
    loop.run_until_complete(asyncio.wait(tasks)) # 注册tasks任务,并启动事件循环
    print("last time:{}".format(time.time()-start_time))

 5、asyncio 模拟 http 请求

 5.1、使用asyncio模拟HTTP请求

 asyncio没有提供http协议的接口, aiohttp有,使用aiohttp模拟HTTP请求代码更简洁

import asyncio
import socket
from urllib.parse import urlparse

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

    #建立socket连接
    connect = asyncio.open_connection(host, 80)  # 建立连接,返回值有两个:reader、writer
    reader, writer = await connect # 拿到reader、writer
    writer.write("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8"))  # 发送数据,相当于sock.send
    all_lines = []
    async for raw_line in reader: # 读取数据(服务器响应数据),使用async使异步化处理
        data = raw_line.decode("utf8")
        all_lines.append(data)
    html = "\n".join(all_lines)
    return html

async def main():
    tasks = []
    for url in range(20):
        url = "http://shop.projectsedu.com/goods/{}/".format(url)
        tasks.append(asyncio.ensure_future(get_url(url)))
    for task in asyncio.as_completed(tasks): # 数据爬取完成的task都在这
        result = await task
        print(result)

if __name__ == "__main__":
    import time
    start_time = time.time()
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    print('last time:{}'.format(time.time()-start_time))

 

5.2、使用 aiohttp 实现HTTP请求

首先需安装aiohttp模块:pip install aiohttp

使用:

import asyncio,aiohttp
import time
async def fetch_async(url):
    """
    异步化,模拟http请求
    遇到await 挂起,转到下一个任务执行。await执行完成,再返回来执行await下面的代码
    """
    print(url)
    async with aiohttp.request("GET",url) as r: # aiohttp 中封装的request函数。发起http请求
        reponse = await r.text(encoding="utf-8") #或者直接await r.read()不编码,直接读取,适合于图像等无法编码文件
        print(reponse) # 代码执行完成会自动释放控制权

tasks = [fetch_async('http://www.baidu.com/'), fetch_async('http://www.chouti.com/'),fetch_async('http://www.chouti.com/'),fetch_async('http://www.chouti.com/'),fetch_async('http://www.chouti.com/')]

start_time = time.time()
event_loop = asyncio.get_event_loop()
results = event_loop.run_until_complete(asyncio.gather(*tasks))
print("time:{}".format((time.time() - start_time)))
event_loop.close()

 


 

 

 

 

posted on 2018-10-12 01:46  Eric_nan  阅读(414)  评论(0编辑  收藏  举报