Python3入门到精通——asyncio 并发编程

作者: Daniel Meng

GitHub: LibertyDream

博客:明月轩

本系列教程采用知识共享署名-非商业性使用-相同方式共享 2.5 中国大陆许可协议

上一章中我们讲到当下实现高并发的主流方式是 事件循环 + 回调 + I/O 复用,不同系统 I/O 复用会有一些差异,Windows 是 select, Linux 是 epoll。回调因为逻辑割裂通常都会用协程驱动的方式代替,Python 为此提供了 asyncawait 原生协程。

所以还欠缺事件循环模块,对此 Python 提供了 asyncio 模块,这也是 Python 应对异步 I/O 编程提供的一整套解决方案。具体而言 asyncio 完成了以下功能:

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

基于 asyncio 开发的框架有很多,比如 tornado,gevent,twisted 等。tornado 自身实现了 web 服务功能,一般和 django + flask + nginx 搭配的比较多

asyncio 的使用也是很便利的,不妨看个例子

In [1]:
import nest_asyncio  # jupyter notebook 本身也运行着一个事件循环,不能嵌套。要用 nest_asyncio
nest_asyncio.apply()
In [3]:
import asyncio
import time
async def get_html(url):
    print('start getting url')
    await asyncio.sleep(2)
    print('task completed')

def main():
    start_time = time.time()
    loop = asyncio.get_event_loop()  # 获取事件循环
    loop.run_until_complete(get_html('http://www.google.com'))
    print(time.time() - start_time)

main()
start getting url
task completed
2.0024678707122803

这里首先用 async 标明协程处理程序,对阻塞操作用 await 标明保证异步执行后续代码。调用函数内 get_event_loop 方法获取事件循环,会持续监听套接字请求,run_until_complete会陷入阻塞,当任务完成恢复执行。

要注意 time.sleep() 是同步方法,直到调用完成前都会陷入阻塞状态,无法实现单线程内的并发。要使用 asyncio.sleep,这是异步的。可以简单验证二者区别,这里要用到 asyncio.wait() 方法,其本身是一个协程,接收可迭代对象做参数,类似于多线程中的 wait,可以指定 ALL_COMPLETEDFIRST_COMPLETEDFIRST_EXCEPTION 三种返回时机,默认为全部任务完成才返回。

In [6]:
import asyncio
import time
async def num_io(num_id):
    print('start processing {}'.format(num_id))
    await asyncio.sleep(2)
    print('task {} completed'.format(num_id))

async def num_io_time(num_id):
    print('start processing {}'.format(num_id))
    time.sleep(2)
    print('task {} completed'.format(num_id))

def main():    
    loop = asyncio.get_event_loop()

    start_time = time.time()
    tasks = [num_io(i) for i in range(5)]
    loop.run_until_complete(asyncio.wait(tasks))
    print(time.time() - start_time)

    print('----------------------------------')

    start_time = time.time()
    tasks = [num_io_time(i) for i in range(5)]
    loop.run_until_complete(asyncio.wait(tasks))
    print(time.time() - start_time)

main()
start processing 4
start processing 1
start processing 2
start processing 3
start processing 0
task 4 completed
task 2 completed
task 0 completed
task 1 completed
task 3 completed
2.0020251274108887
----------------------------------
start processing 4
task 4 completed
start processing 0
task 0 completed
start processing 1
task 1 completed
start processing 3
task 3 completed
start processing 2
task 2 completed
10.006985664367676

可以看到使用 time.sleep 使程序退化为同步阻塞形式,没有实现异步并发

除了 asyncio.wait(),Python 提供了更高层的抽象等待方法 asyncio.gather(),能够分组执行任务,分组取消任务,批量传入时注意要解包

In [6]:
import asyncio

async def num_io(num_id):
    print('start processing {}'.format(num_id))
    await asyncio.sleep(2)
    print('task {} completed'.format(num_id))

async def str_io(str_id):
    print('start processing {}'.format(str_id))
    await asyncio.sleep(2)
    print('{} is OK'.format(str_id))

def main():    
    loop = asyncio.get_event_loop()

    task_1 = [num_io(i) for i in range(3)]
    task_2 = [str_io('str_'+str(i)) for i in range(3)]
    loop.run_until_complete(asyncio.gather(*task_1, *task_2))  # 注意解包

    print('--------------------')

    task_1 = [num_io(i) for i in range(3,6)]
    task_2 = [str_io('str_'+str(i)) for i in range(3,6)]
    task_1 = asyncio.gather(*task_1)  # 先建局部再统一传入也都可以
    task_2 = asyncio.gather(*task_2)
    task_1.cancel()  # 取消一组任务
    loop.run_until_complete(asyncio.gather(task_2))  # 不需要再解包了,如果再传入 task_1 会报 CancelledError
main()
start processing 0
start processing 1
start processing 2
start processing str_0
start processing str_1
start processing str_2
task 0 completed
task 2 completed
str_2 is OK
str_1 is OK
task 1 completed
str_0 is OK
--------------------
start processing str_3
start processing str_4
start processing str_5
str_3 is OK
str_4 is OK
str_5 is OK

获取返回值

驱动协程完成任务少不了接收返回值,有两种方法可选 asyncio.ensure_future(),类似多线程,返回一个 Future 对象,event_loop_obj.create_task(),返回 task 对象,Future、task 使用方法相同都能调用其中的 .result() 获取协程返回值。

TaskFuture 的子类,主要是将协程包装进 future 当中。因为生成器实现协程必须在一开始执行一次 send(None),同时任务结束后要处理 StopIteration 异常并存储返回值,这些工作线程返回对象 future 无法实现,于是进一步封装出了 Task

task 对象可以通过 add_done_callback() 指定回调方法,协程任务完成后执行

In [8]:
import asyncio

async def num_io(num_id):
    print('start processing {}'.format(num_id))
    await asyncio.sleep(2)
    print('task {} completed'.format(num_id))
    return 'Coroutine Finished'

def call_back(future):
    print('success calling call_back method')

def main():    
    loop = asyncio.get_event_loop()

    future = asyncio.ensure_future(num_io(1))
    loop.run_until_complete(future)
    print('future:{}'.format(future.result()))
    print('--------------------')
    task = loop.create_task(num_io(2))
    task.add_done_callback(call_back)
    loop.run_until_complete(task)
    print('task:{}'.format(task.result()))

main()
start processing 1
task 1 completed
future:Coroutine Finished
--------------------
start processing 2
task 2 completed
success calling call_back method
task:Coroutine Finished

回调方法可能需要传入参数,这时可以使用 funcitontools.partial(func,args) 将调用函数与所需参数进行封装,组装成新方法作回调用

In [11]:
import asyncio
from functools import partial

async def num_io(num_id):
    print('start processing {}'.format(num_id))
    await asyncio.sleep(2)
    print('task {} completed'.format(num_id))
    return 'Coroutine Finished'

def call_back(args, future):  # 参数在前,future 在后
    print('call_back method get args:{}'.format(args))

def main():    
    loop = asyncio.get_event_loop()

    task = loop.create_task(num_io(1))
    task.add_done_callback(partial(call_back,'Hello World'))
    loop.run_until_complete(task)
    print('task:{}'.format(task.result()))

main()
start processing 1
task 1 completed
call_back method get args:Hello World
task:Coroutine Finished

取消任务

生产环境中经常面临因某些原因要中断任务执行。这里涉及到两点,一是断开所有任务,二是停止事件循环。一般启动事件循环时会使用 loop.run_forever() 方法一直循环下去,而 loop.run_until_complete() 则是在此基础上通过回调,在任务完成后从 future 对象内获取 loop 对象调用 stop() 方法将循环关闭

下面以 CTRL + C 中断为例

In [ ]:
import asyncio
'''stop_loop.py
    终端运行,jupyter 自身有 loop 不能 close()
'''

async def nip(times):
    print('go to bed')
    await asyncio.sleep(times)
    print('get up after {}s'.format(times))

def main():
    loop = asyncio.get_event_loop()
    tasks = [nip(times) for times in range(1,5)]
    try:
        loop.run_until_complete(asyncio.wait(tasks))
    except KeyboardInterrupt:
        all_tasks = asyncio.Task.all_tasks()
        for task in all_tasks:
            print('task cancel:{}'.format(task.cancel()))
        loop.stop()
        loop.run_forever()
    finally:
        loop.close()

main()

这里定义了 4 个睡眠任务异步执行,终端传入中断指令后捕获 KeyboardInterrupt 异常。asyncio 模块下专门有 Task 类管理任务,其 all_tasks() 方法会找到 loop 对象(如果没有就新建),返回循环列表中的所有任务。之后遍历任务列表,调用 cancel() 方法取消任务,之后停止循环。

注意 loop.stop() 方法只是改变事件循环内部停止标识,标识后循环任务销毁但 loop 仍处于就绪态,所以要调用 run_forever() 方法使其恢复正常,最后执行 loop.close() 关闭循环,close 会清空就绪、计划队列,然后结束进程。

协程嵌套

之前通过生成其展示过调用者和子生成器间建立双通“管道”的过程。这里还是举例说明 asyncio 模块下的协程嵌套

In [6]:
import asyncio

async def compute(x, y):
  print("Compute %s + %s ..." % (x, y))
  await asyncio.sleep(1.0)
  return x + y

async def print_sum(x, y):
  result = compute(x, y)
  print("%s + %s = %s" % (x, y, result))

loop = asyncio.get_event_loop()
loop.run_until_complete(print_sum(1, 2))
1 + 2 = <coroutine object compute at 0x000001BCF3892648>

时序图如下

事件循环启动后,指定监听任务 print_sumprint_sum开始执行,调用协程 compute 完成求和计算,而compute 内部又调用 sleep,陷入阻塞,注意这里没有经过“委托人”print_sum,直接送回调用者。恢复执行后,调用者直接向子协程收取结果,返回 1+2compute 调用完毕,print_sum 收到停止信号,打印输出后 print_sum 协程任务也已执行完毕,继续上报。整个任务执行完毕,loop.close() 关闭事件循环

call 系列方法

asyncio 允许定制任务在事件循环中执行的时机,对应于

  • call_soon(call_back_func, *args): 下次循环立刻执行
  • call_at(loop_times, call_back_func, *args): 事件循环中的某个时刻执行
  • call_later(delay_times, call_back_func, *args): 几秒钟后执行,自动按时间升序执行
  • call_soon_threadsafe: 效果同 call_soon 且保证线程安全
In [3]:
import asyncio

def call_back(times):
    print('I want to sleep {}s'.format(times))

def stop_loop(loop):
    loop.stop()

loop = asyncio.get_event_loop()
loop.call_soon(call_back, 2)
loop.call_soon(stop_loop,loop)  # call_back 的下一次循环停止 loop 循环
loop.run_forever()
I want to sleep 2s
In [6]:
import asyncio

def call_back(times):
    print('{}s has gone'.format(times))

def stop_loop(loop):
    loop.stop()

loop = asyncio.get_event_loop()
loop.call_later(3, call_back, 3)  # 会自动按时间重排
loop.call_later(1, call_back, 1)
loop.call_later(2, call_back, 2)
loop.call_later(4, stop_loop,loop)
loop.run_forever()
1s has gone
2s has gone
3s has gone
In [9]:
import asyncio

def call_back(times):
    print('another {}s has gone'.format(times))

def stop_loop(loop):
    loop.stop()

loop = asyncio.get_event_loop()
now = loop.time()  # 注意是循环内的时间
loop.call_at(now + 3, call_back, 3)  # 会自动按时间重排
loop.call_at(now + 1, call_back, 1)
loop.call_at(now + 2, call_back, 2)
loop.call_at(now + 4, stop_loop,loop)
loop.run_forever()
another 1s has gone
another 2s has gone
another 3s has gone

asyncio 与多线程

I/O 复用下有时依旧不能避免阻塞 I/O 操作,比如数据库读写。asyncio 作为一整套异步解决方案,提供了线程池的接口run_in_executor(executor, func, *args),对于必须处理的同步操作可以放到线程池里交给其他线程处理

In [12]:
import asyncio
import socket
import time
from concurrent.futures import ThreadPoolExecutor
from urllib.parse import urlparse


def get_url(url):

    url = urlparse(url)
    host = url.netloc
    path = url.path
    if path == "":
        path = "/"

    #  IPv4,TCP 连接
    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()



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)
    tasks.append(task)
loop.run_until_complete(asyncio.wait(tasks))
print("last time:{}".format(time.time()-start_time))
es":[{"image":"http://shop.projectsedu.com/media/goods/images/7_P_1448945104883.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/7_P_1448945104734.jpg"}],"goods_sn":"","name":"酣畅家庭菲力牛排10片澳洲生鲜牛肉团购套餐","click_num":2588,"sold_num":0,"fav_num":0,"goods_num":0,"market_price":286.0,"shop_price":238.0,"goods_brief":"","goods_desc":"<p><img src=\"/media/goods/images/2_20170719161405_249.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161414_628.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161435_381.jpg\" title=\"\" alt=\"2.jpg\"/></p>","ship_free":true,"goods_front_image":"http://shop.projectsedu.com/media/goods/images/7_P_1448945104883.jpg","is_new":false,"is_hot":false,"add_time":"2017-07-31T23:53:53"}
{"id":4,"category":{"id":129,"sub_cat":[],"name":"根茎类","code":"gjl","desc":"","category_type":2,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":110},"images":[{"image":"http://shop.projectsedu.com/media/goods/images/47_P_1448946213263.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/47_P_1448946213157.jpg"}],"goods_sn":"","name":"日本蒜蓉粉丝扇贝270克6只装","click_num":2614,"sold_num":0,"fav_num":-1,"goods_num":0,"market_price":156.0,"shop_price":108.0,"goods_brief":"","goods_desc":"<p><img src=\"/media/goods/images/2_20170719161405_249.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161414_628.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161435_381.jpg\" title=\"\" alt=\"2.jpg\"/></p>","ship_free":true,"goods_front_image":"http://shop.projectsedu.com/media/goods/images/47_P_1448946213263.jpg","is_new":false,"is_hot":false,"add_time":"2017-07-31T23:53:53"}
{"id":6,"category":{"id":130,"sub_cat":[],"name":"茄果类","code":"qgl","desc":"","category_type":2,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":110},"images":[{"image":"http://shop.projectsedu.com/media/goods/images/4_P_1448945381985.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/4_P_1448945381013.jpg"}],"goods_sn":"","name":"乌拉圭进口牛肉卷特级肥牛卷","click_num":2432,"sold_num":0,"fav_num":0,"goods_num":0,"market_price":90.0,"shop_price":75.0,"goods_brief":"","goods_desc":"<p><img src=\"/media/goods/images/2_20170719161405_249.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161414_628.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161435_381.jpg\" title=\"\" alt=\"2.jpg\"/></p>","ship_free":true,"goods_front_image":"http://shop.projectsedu.com/media/goods/images/4_P_1448945381985.jpg","is_new":false,"is_hot":false,"add_time":"2017-07-31T23:53:53"}{"id":5,"category":{"id":116,"sub_cat":[{"id":117,"sub_cat":[],"name":"参鲍","code":"cb","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":116},{"id":118,"sub_cat":[],"name":"鱼","code":"yu","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":116},{"id":119,"sub_cat":[],"name":"虾","code":"xia","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":116},{"id":120,"sub_cat":[],"name":"蟹/贝","code":"xb","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":116}],"name":"海鲜水产","code":"hxsc","desc":"","category_type":2,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":110},"images":[{"image":"http://shop.projectsedu.com/media/goods/images/10_P_1448944572085.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/10_P_1448944572532.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/10_P_1448944572872.jpg"}],"goods_sn":"","name":"内蒙新鲜牛肉1斤清真生鲜牛肉火锅食材","click_num":2440,"sold_num":0,"fav_num":0,"goods_num":0,"market_price":106.0,"shop_price":88.0,"goods_brief":"","goods_desc":"<p><img src=\"/media/goods/images/2_20170719161405_249.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161414_628.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161435_381.jpg\" title=\"\" alt=\"2.jpg\"/></p>","ship_free":true,"goods_front_image":"http://shop.projectsedu.com/media/goods/images/10_P_1448944572085.jpg","is_new":false,"is_hot":false,"add_time":"2017-07-31T23:53:53"}

{"id":7,"category":{"id":132,"sub_cat":[],"name":"进口生鲜","code":"jksx","desc":"","category_type":2,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":110},"images":[{"image":"http://shop.projectsedu.com/media/goods/images/8_P_1448945032810.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/8_P_1448945032646.jpg"}],"goods_sn":"","name":"五星眼肉牛排套餐8片装原味原切生鲜牛肉","click_num":2369,"sold_num":0,"fav_num":0,"goods_num":0,"market_price":150.0,"shop_price":125.0,"goods_brief":"","goods_desc":"<p><img src=\"/media/goods/images/2_20170719161405_249.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161414_628.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161435_381.jpg\" title=\"\" alt=\"2.jpg\"/></p>","ship_free":true,"goods_front_image":"http://shop.projectsedu.com/media/goods/images/8_P_1448945032810.jpg","is_new":false,"is_hot":false,"add_time":"2017-07-31T23:53:53"}
{"id":9,"category":{"id":131,"sub_cat":[],"name":"菌菇类","code":"jgl","desc":"","category_type":2,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":110},"images":[{"image":"http://shop.projectsedu.com/media/goods/images/6_P_1448945167279.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/6_P_1448945167015.jpg"}],"goods_sn":"","name":"潮香村澳洲进口牛排家庭团购套餐20片","click_num":2369,"sold_num":0,"fav_num":0,"goods_num":0,"market_price":239.0,"shop_price":199.0,"goods_brief":"","goods_desc":"<p><img src=\"/media/goods/images/2_20170719161405_249.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161414_628.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161435_381.jpg\" title=\"\" alt=\"2.jpg\"/></p>","ship_free":true,"goods_front_image":"http://shop.projectsedu.com/media/goods/images/6_P_1448945167279.jpg","is_new":false,"is_hot":false,"add_time":"2017-07-31T23:53:53"}
{"id":8,"category":{"id":116,"sub_cat":[{"id":117,"sub_cat":[],"name":"参鲍","code":"cb","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":116},{"id":118,"sub_cat":[],"name":"鱼","code":"yu","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":116},{"id":119,"sub_cat":[],"name":"虾","code":"xia","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":116},{"id":120,"sub_cat":[],"name":"蟹/贝","code":"xb","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":116}],"name":"海鲜水产","code":"hxsc","desc":"","category_type":2,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":110},"images":[{"image":"http://shop.projectsedu.com/media/goods/images/11_P_1448944388277.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/11_P_1448944388034.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/11_P_1448944388201.jpg"}],"goods_sn":"","name":"澳洲进口120天谷饲牛仔骨4份原味生鲜","click_num":2354,"sold_num":0,"fav_num":0,"goods_num":0,"market_price":31.0,"shop_price":26.0,"goods_brief":"","goods_desc":"<p><img src=\"/media/goods/images/2_20170719161405_249.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161414_628.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161435_381.jpg\" title=\"\" alt=\"2.jpg\"/></p>","ship_free":true,"goods_front_image":"http://shop.projectsedu.com/media/goods/images/11_P_1448944388277.jpg","is_new":false,"is_hot":false,"add_time":"2017-07-31T23:53:53"}
{"id":10,"category":{"id":129,"sub_cat":[],"name":"根茎类","code":"gjl","desc":"","category_type":2,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":110},"images":[{"image":"http://shop.projectsedu.com/media/goods/images/9_P_1448944791617.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/9_P_1448944791129.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/9_P_1448944791077.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/9_P_1448944791229.jpg"}],"goods_sn":"","name":"爱食派内蒙古呼伦贝尔冷冻生鲜牛腱子肉1000g","click_num":2337,"sold_num":0,"fav_num":0,"goods_num":0,"market_price":202.0,"shop_price":168.0,"goods_brief":"","goods_desc":"<p><img src=\"/media/goods/images/2_20170719161405_249.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161414_628.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161435_381.jpg\" title=\"\" alt=\"2.jpg\"/></p>","ship_free":true,"goods_front_image":"http://shop.projectsedu.com/media/goods/images/9_P_1448944791617.jpg","is_new":false,"is_hot":false,"add_time":"2017-07-31T23:53:53"}
{"id":11,"category":{"id":111,"sub_cat":[{"id":112,"sub_cat":[],"name":"羊肉","code":"yr","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":111},{"id":113,"sub_cat":[],"name":"禽类","code":"ql","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":111},{"id":114,"sub_cat":[],"name":"猪肉","code":"zr","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":111},{"id":115,"sub_cat":[],"name":"牛肉","code":"nr","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":111}],"name":"精品肉类","code":"jprl","desc":"","category_type":2,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":110},"images":[{"image":"http://shop.projectsedu.com/media/goods/images/3_P_1448945490837.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/3_P_1448945490084.jpg"}],"goods_sn":"","name":"澳洲进口牛尾巴300g新鲜肥牛肉","click_num":2407,"sold_num":0,"fav_num":0,"goods_num":0,"market_price":306.0,"shop_price":255.0,"goods_brief":"新鲜羊羔肉整只共15斤,原生态大山放牧羊羔,曾经的皇室贡品,央视推荐,2005年北京招待全球财金首脑。五层专用包装箱+真空包装+冰袋+保鲜箱+顺丰冷链发货,路途保质期8天","goods_desc":"<p><img src=\"/media/goods/images/2_20170719161405_249.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161414_628.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161435_381.jpg\" title=\"\" alt=\"2.jpg\"/></p>","ship_free":true,"goods_front_image":"http://shop.projectsedu.com/media/goods/images/3_P_1448945490837.jpg","is_new":false,"is_hot":false,"add_time":"2017-07-31T23:53:53"}
{"id":12,"category":{"id":116,"sub_cat":[{"id":117,"sub_cat":[],"name":"参鲍","code":"cb","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":116},{"id":118,"sub_cat":[],"name":"鱼","code":"yu","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":116},{"id":119,"sub_cat":[],"name":"虾","code":"xia","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":116},{"id":120,"sub_cat":[],"name":"蟹/贝","code":"xb","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":116}],"name":"海鲜水产","code":"hxsc","desc":"","category_type":2,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":110},"images":[{"image":"http://shop.projectsedu.com/media/goods/images/48_P_1448943988970.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/48_P_1448943988898.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/48_P_1448943988439.jpg"}],"goods_sn":"","name":"新疆巴尔鲁克生鲜牛排眼肉牛扒1200g","click_num":2308,"sold_num":0,"fav_num":0,"goods_num":0,"market_price":126.0,"shop_price":88.0,"goods_brief":"","goods_desc":"<p><img src=\"/media/goods/images/2_20170719161405_249.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161414_628.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161435_381.jpg\" title=\"\" alt=\"2.jpg\"/></p>","ship_free":true,"goods_front_image":"http://shop.projectsedu.com/media/goods/images/48_P_1448943988970.jpg","is_new":false,"is_hot":false,"add_time":"2017-07-31T23:53:53"}
{"id":14,"category":{"id":130,"sub_cat":[],"name":"茄果类","code":"qgl","desc":"","category_type":2,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":110},"images":[{"image":"http://shop.projectsedu.com/media/images/201705/goods_img/53_P_1495068879687.jpg"}],"goods_sn":"","name":"帐篷出租","click_num":2364,"sold_num":0,"fav_num":0,"goods_num":0,"market_price":120.0,"shop_price":100.0,"goods_brief":"","goods_desc":"<p><img src=\"/media/goods/images/2_20170719161405_249.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161414_628.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161435_381.jpg\" title=\"\" alt=\"2.jpg\"/></p>","ship_free":true,"goods_front_image":"http://shop.projectsedu.com/media/goods/images/53_P_1495068879687.jpg","is_new":false,"is_hot":false,"add_time":"2017-07-31T23:53:54"}
{"id":13,"category":{"id":121,"sub_cat":[{"id":122,"sub_cat":[],"name":"松花蛋/咸鸭蛋","code":"xhd_xyd","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":121},{"id":123,"sub_cat":[],"name":"鸡蛋","code":"jd","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":121}],"name":"蛋制品","code":"dzp","desc":"","category_type":2,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":110},"images":[{"image":"http://shop.projectsedu.com/media/goods/images/5_P_1448945270390.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/5_P_1448945270067.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/5_P_1448945270432.jpg"}],"goods_sn":"","name":"澳洲进口安格斯牛切片上脑牛排1000g","click_num":2349,"sold_num":0,"fav_num":0,"goods_num":-1,"market_price":144.0,"shop_price":120.0,"goods_brief":"澳大利亚是国际公认的没有疯牛病和口蹄疫的国家。为了保持澳大利亚产品的高标准,澳大利亚牛肉业和各级政府共同努力简历了严格的标准和体系,以保证生产的整体化和产品的可追溯性","goods_desc":"<p><img src=\"/media/goods/images/2_20170719161405_249.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161414_628.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161435_381.jpg\" title=\"\" alt=\"2.jpg\"/></p>","ship_free":true,"goods_front_image":"http://shop.projectsedu.com/media/goods/images/5_P_1448945270390.jpg","is_new":false,"is_hot":false,"add_time":"2017-07-31T23:53:54"}
{"id":16,"category":{"id":145,"sub_cat":[],"name":"饮料/水","code":"yls","desc":"","category_type":2,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":133},"images":[{"image":"http://shop.projectsedu.com/media/goods/images/14_P_1448947354031.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/14_P_1448947354433.jpg"}],"goods_sn":"","name":"52度水井坊臻酿八號500ml","click_num":2372,"sold_num":0,"fav_num":0,"goods_num":-3,"market_price":43.0,"shop_price":36.0,"goods_brief":"","goods_desc":"<p><img src=\"/media/goods/images/2_20170719161405_249.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161414_628.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161435_381.jpg\" title=\"\" alt=\"2.jpg\"/></p>","ship_free":true,"goods_front_image":"http://shop.projectsedu.com/media/goods/images/14_P_1448947354031.jpg","is_new":false,"is_hot":false,"add_time":"2017-07-31T23:53:54"}
{"id":15,"category":{"id":146,"sub_cat":[{"id":147,"sub_cat":[],"name":"白兰地","code":"bld","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":146},{"id":148,"sub_cat":[],"name":"威士忌","code":"wsj","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":146}],"name":"红酒","code":"hj","desc":"","category_type":2,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":133},"images":[{"image":"http://shop.projectsedu.com/media/goods/images/16_P_1448947194687.jpg"}],"goods_sn":"","name":"52度茅台集团国隆双喜酒500mlx6","click_num":2504,"sold_num":0,"fav_num":0,"goods_num":-5,"market_price":23.0,"shop_price":19.0,"goods_brief":"贵州茅台酒厂(集团)保健酒业有限公司生产,是以“龙”字打头的酒水。中国龙文化上下8000年,源远而流长,龙的形象是一种符号、一种意绪、一种血肉相联的情感。","goods_desc":"<p><img src=\"/media/goods/images/2_20170719161405_249.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161414_628.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161435_381.jpg\" title=\"\" alt=\"2.jpg\"/></p>","ship_free":true,"goods_front_image":"http://shop.projectsedu.com/media/goods/images/16_P_1448947194687.jpg","is_new":false,"is_hot":false,"add_time":"2017-07-31T23:53:54"}
{"id":17,"category":{"id":141,"sub_cat":[{"id":142,"sub_cat":[],"name":"其他品牌","code":"qtpp","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":141},{"id":143,"sub_cat":[],"name":"黄酒","code":"hj","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":141},{"id":144,"sub_cat":[],"name":"养生酒","code":"ysj","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":141}],"name":"其他酒品","code":"qtjp","desc":"","category_type":2,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":133},"images":[{"image":"http://shop.projectsedu.com/media/goods/images/12_P_1448947547989.jpg"}],"goods_sn":"","name":"53度茅台仁酒500ml","click_num":2304,"sold_num":0,"fav_num":0,"goods_num":0,"market_price":190.0,"shop_price":158.0,"goods_brief":"","goods_desc":"<p><img src=\"/media/goods/images/2_20170719161405_249.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161414_628.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161435_381.jpg\" title=\"\" alt=\"2.jpg\"/></p>","ship_free":true,"goods_front_image":"http://shop.projectsedu.com/media/goods/images/12_P_1448947547989.jpg","is_new":false,"is_hot":false,"add_time":"2017-07-31T23:53:54"}
{"id":18,"category":{"id":138,"sub_cat":[],"name":"葡萄酒","code":"ptj","desc":"","category_type":2,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":133},"images":[{"image":"http://shop.projectsedu.com/media/goods/images/46_P_1448946598711.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/46_P_1448946598301.jpg"}],"goods_sn":"","name":"双响炮洋酒JimBeamwhiskey美国白占边","click_num":2323,"sold_num":0,"fav_num":0,"goods_num":0,"market_price":38.0,"shop_price":28.0,"goods_brief":"","goods_desc":"<p><img src=\"/media/goods/images/2_20170719161405_249.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161414_628.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161435_381.jpg\" title=\"\" alt=\"2.jpg\"/></p>","ship_free":true,"goods_front_image":"http://shop.projectsedu.com/media/goods/images/46_P_1448946598711.jpg","is_new":false,"is_hot":false,"add_time":"2017-07-31T23:53:54"}
{"id":19,"category":{"id":145,"sub_cat":[],"name":"饮料/水","code":"yls","desc":"","category_type":2,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":133},"images":[{"image":"http://shop.projectsedu.com/media/goods/images/21_P_1448946793276.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/21_P_1448946793153.jpg"}],"goods_sn":"","name":"西夫拉姆进口洋酒小酒版","click_num":2301,"sold_num":0,"fav_num":0,"goods_num":0,"market_price":55.0,"shop_price":46.0,"goods_brief":"","goods_desc":"<p><img src=\"/media/goods/images/2_20170719161405_249.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161414_628.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161435_381.jpg\" title=\"\" alt=\"2.jpg\"/></p>","ship_free":true,"goods_front_image":"http://shop.projectsedu.com/media/goods/images/21_P_1448946793276.jpg","is_new":false,"is_hot":false,"add_time":"2017-07-31T23:53:54"}
last time:0.4722774028778076

asyncio 模拟 http

我们曾通过 selectors 借助函数回调模拟 http 协议执行过程,有了 asyncio 后借助协程可以更加简便的实现这一目的

In [13]:
import asyncio
import socket
import time
from urllib.parse import urlparse


async def get_url(url):

    url = urlparse(url)
    host = url.netloc
    path = url.path
    if path == "":
        path = "/"

    reader, writer = await asyncio.open_connection(host,80)
    writer.write("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8"))
    all_lines = []
    async for raw_line in reader:
        data = raw_line.decode("utf8")
        all_lines.append(data)
    html = "\n".join(all_lines)
    return html

async def main():
    tasks = []
    for url in range(3):
        url = "http://shop.projectsedu.com/goods/{}/".format(url)
        tasks.append(asyncio.ensure_future(get_url(url)))
    for task in asyncio.as_completed(tasks):
        result = await task
        print(result)


start_time = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
print('last time:{}'.format(time.time()-start_time))
HTTP/1.1 404 Not Found

Server: nginx/1.16.1

Date: Sat, 29 Feb 2020 14:12:11 GMT

Content-Type: application/json

Content-Length: 25

Connection: close

Vary: Accept, Cookie

Allow: GET, HEAD, OPTIONS

X-Frame-Options: SAMEORIGIN



{"detail":"未找到。"}
HTTP/1.1 200 OK

Server: nginx/1.16.1

Date: Sat, 29 Feb 2020 14:12:11 GMT

Content-Type: application/json

Content-Length: 1497

Connection: close

Vary: Accept, Cookie

Allow: GET, HEAD, OPTIONS

X-Frame-Options: SAMEORIGIN



{"id":1,"category":{"id":129,"sub_cat":[],"name":"根茎类","code":"gjl","desc":"","category_type":2,"is_tab":false,"add_time":"2017-07-29T18:56:34","parent_category":110},"images":[{"image":"http://shop.projectsedu.com/media/goods/images/1_P_1449024889889.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/1_P_1449024889264.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/1_P_1449024889726.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/1_P_1449024889018.jpg"},{"image":"http://shop.projectsedu.com/media/goods/images/1_P_1449024889287.jpg"}],"goods_sn":"","name":"新鲜水果甜蜜香脆单果约800克","click_num":4357,"sold_num":0,"fav_num":0,"goods_num":-16,"market_price":232.0,"shop_price":156.0,"goods_brief":"食用百香果可以增加胃部饱腹感,减少余热量的摄入,还可以吸附胆固醇和胆汁之类有机分子,抑制人体对脂肪的吸收。因此,长期食用有利于改善人体营养吸收结构,降低体内脂肪,塑造健康优美体态。","goods_desc":"<p><img src=\"/media/goods/images/2_20170719161405_249.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161414_628.jpg\" title=\"\" alt=\"2.jpg\"/></p><p><img src=\"/media/goods/images/2_20170719161435_381.jpg\" title=\"\" alt=\"2.jpg\"/></p>","ship_free":true,"goods_front_image":"http://shop.projectsedu.com/media/goods/images/1_P_1449024889889.jpg","is_new":false,"is_hot":false,"add_time":"2017-07-31T23:53:52"}
HTTP/1.1 200 OK

Server: nginx/1.16.1

Date: Sat, 29 Feb 2020 14:12:11 GMT

Content-Type: application/json

Content-Length: 1964

Connection: close

Vary: Accept, Cookie

Allow: GET, HEAD, OPTIONS

X-Frame-Options: SAMEORIGIN



{"id":2,"category":{"id":116,"sub_cat":[{"id":117,"sub_cat":[],"name":"参鲍","code":"cb","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":116},{"id":118,"sub_cat":[],"name":"鱼","code":"yu","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":116},{"id":119,"sub_cat":[],"name":"虾","code":"xia","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":116},{"id":120,"sub_cat":[],"name":"蟹/贝","code":"xb","desc":"","category_type":3,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":116}],"name":"海鲜水产","code":"hxsc","desc":"","category_type":2,"is_tab":false,"add_time":"2017-07-29T18:56:33","parent_category":110},"images":[{"image":"http://shop.projectsedu.com/media/goods/images/2_P_1448945810202.jpg"},{"image":"http://shop.projectsedu.com/media/15_P_1448947257324.jpg"},{"image":"http://shop.projectsedu.com/media/2_20170719161405_249.jpg"},{"image":"http://shop.projectsedu.com/media/9_P_1448944791617.jpg"}],"goods_sn":"sssss","name":"田然牛肉大黄瓜条生鲜牛肉冷冻真空黄牛","click_num":3504,"sold_num":100,"fav_num":0,"goods_num":-5,"market_price":106.0,"shop_price":88.0,"goods_brief":"前腿+后腿+羊排共8斤,原生态大山放牧羊羔,曾经的皇室贡品,央视推荐,2005年北京招待全球财金首脑。五层专用包装箱+真空包装+冰袋+保鲜箱+顺丰冷链发货,路途保质期8天","goods_desc":"<p><img src=\"/media/goods/images/2_20170719161405_249.jpg\" title=\"\" alt=\"2.jpg\"/> </p><p><img src=\"/media/goods/images/2_20170719161414_628.jpg\" title=\"\" alt=\"2.jpg\"/> </p><p><img src=\"/media/goods/images/2_20170719161435_381.jpg\" title=\"\" alt=\"2.jpg\"/> </p>","ship_free":true,"goods_front_image":"http://shop.projectsedu.com/media/goods/images/2_P_1448945810202.jpg","is_new":false,"is_hot":false,"add_time":"2017-07-31T23:53:00"}
last time:0.3327338695526123

首先,asyncio 提供了协程 open_connection(),不再需要手动通过 socket.socket 指定协议请求连接了,免去阻塞困扰的同时协程会返回处理读写请求的两个对象。async for 能将 for 循环接收读取内容的工作异步化,防止有同步操作阻塞执行,影响效率。

为了达到完成一个任务处理一个任务的效果,asyncio 也提供了 as_completed() 方法,但要注意因为任务都是异步的,要加上 await 等待任务结果。

asyncio 同步与通信

介绍 GIL 时我们举例说明 GIL 会因 I/O 操作、时间片耗尽和字节码情况主动释放,导致变量值震荡。但 asyncio 虽是异步,却能保证在非 I/O 阻塞场景下变量值的稳定

In [4]:
var_a = 0

async def add():
    global var_a
    for i in range(1000000):
        var_a += 1

async def subtract():
    global var_a
    for i in range(1000000):
        var_a -= 1

import asyncio

loop =  asyncio.get_event_loop()
tasks = [add(), subtract()]
loop.run_until_complete(asyncio.wait(tasks))
print(var_a)
0

其中原因是,异步并发是单线程的,除非涉及到阻塞 I/O 操作会调用协程转到其他方法,其余计算任务都会全部执行完(至函数体结束)才转到其他方法

但异步场景下有时也会面临同步困扰,比如两个异步程序同时调用了一个协程,为了数据安全、鲁棒需要锁机制。但异步程序杜绝使用阻塞方法,比如 Python 的 lock,于是 asyncio 提供了代码级的锁 asyncio.Lock,不调用 Python 原生的锁,只是创立一个标识,访问前检测标识状态,以此保证冲突资源的线性访问。

基于同样的道理 asyncio 还提供了异步的双端队列 asyncio.Queuegetput 方法使用前也要加上 await

In [ ]:
import asyncio
from asyncio import Lock  # 注意是 asyncio 下的锁

cache = {}
lock = Lock()

async def parse_url(url):
    await asyncio.sleep(2)
    return 'decorate {' + str(url) + '}'

async def get_stuff(url):
    async with lock.acquire():
        if url in cache:
            return cache[url]
        stuff = await parse_url(url)
        return stuff

async def use_stuff(url):
    stuff = await get_stuff(url)
    # do something
    pass

async def parse_stuff(url):
    stuff = await get_stuff(url)
    # do something
    pass

伪代码展示的是当 use_stuffparse_stuff 都被添加到事件循环中时,可能存在的访问冲突,二者都尝试访问 get_stuff 获取对象,这里使用 lock.acquire() 对获取过程加锁保证顺序性。

注意这里的 acquire 也是一个协程,同时实现了上下文管理器协议 __exit____enter__,所以可以使用 with await 方式,而 asyncio 为了保证语义清晰,提供了 __await____aenter__ 协议应对这种情况,于是有了 asyncio with 语法。