Python 彻底解读协程与异步【看完包会】

python项目实战——多协程下载美女图片 I/O 密集型任务(I/O-bound task)是指那些在执行过程中主要受限于输入/输出(I/O)操作性能的任务。这类任务的执行效率往往取决于数据的读写速度,而不是 CPU 处理能力。I/O 密集型任务是在执行中高度依赖于数据交换和处理速度的任务。理解 I/O 密集型任务的概念有助于在设计和优化系统时选择合适的编程模型和架构。因为爬虫主要就是和网络进行交互,不太消耗我们自己电脑的CPU,负责接收网路返回的数据,是一种写入操作,就是适合爬虫。 阅读详情

title: Python 协程与异步
copyright: true
top: 0
date: 2018-08-11 10:15:50
tags:
categories: Python进阶笔记
permalink:
password:
keywords: 协程
description: Python2.7中用代码实现协程,同时区分同步与异步,以及异步的表现形式,回调与协程。

像烟花也是过一生,像樱花也是过一生,只要亮过和盛开过不就好了吗?

同步异步概念

当提到同步与异步,大家不免会想到另一组词语:阻塞与非阻塞。通常,同时提到这个这几个词语一般实在讨论network io的时候,在《unix network programming》中有详尽的解释,网络中也有许多讲解生动的文章。

关于异步 同步的一些理解:

同步和异步的区别就在于是否等待IO执行的结果。好比你去麦当劳点餐,你说“来个汉堡”,服务员告诉你,对不起,汉堡要现做,需要等5分钟,于是你站在收银台前面等了5分钟,拿到汉堡再去逛商场,这是同步IO。

你说“来个汉堡”,服务员告诉你,汉堡需要等5分钟,你可以先去逛商场,等做好了,我们再通知你,这样你可以立刻去干别的事情(逛商场),这是异步IO。

老张爱喝茶,废话不说,煮开水。出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。1 老张把水壶放到火上,立等水开。(同步阻塞)老张觉得自己有点傻2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。3 老张把响水壶放到火上,立等水开。(异步阻塞)老张觉得这样傻等意义不大4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)老张觉得自己聪明了。所谓同步异步,只是对于水壶而言。普通水壶,同步;响水壶,异步。虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。所谓阻塞非阻塞,仅仅对于老张而言。立等的老张,阻塞;看电视的老张,非阻塞。情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。

并发通常指有多个任务需要同时进行,并行则是同一时刻有多个任务执行。用多线程、多进程、协程来说,协程实现并发,多线程与多进程实现并行。

关于异步 同步堵塞的一些理解:

同步阻塞,就好比火车站过安检,需要你耗费几分钟时间,都检查完了再进站,每个人都要耽误几分钟。

同步非阻塞,我们假设火车站提供了一种服务名叫“反馈”,你交10块钱就可以加一个微信号,然后你把车票、身份证、行李一次性放到一个地方,同时人家还保存了一下你的美照(这一系列操作后面统称“打包”),这样你可以直接进站买点东西呀上个厕所呀(后面统称“闲逛”),再通过微信不断询问 我的票检查好了吗? 查好了吗? 直到那头回复你说“好了”,你到指定地点去把你刚才打的包取回(后面统称“取包”),结束。

异步阻塞,你交20块钱买了“反馈2.0”—检查完毕人家会主动发微信告诉你,不需要你在不断询问了,而你“打包”完毕,还在检票口傻等,直到人家说“好了”,你在“取包”。这其实没有任何意义,因为你还是在等待,真正有意义的是异步非阻塞。

异步非阻塞,你交20块钱买了“反馈2.0”,“打包”完毕,“闲逛”,直到人家说“好了”,然后你“取包”。这才是真正把你解放了,既不用等着,也不用不断询问。而本文的asyncio用的就是异步非阻塞的协程。

协程

优点

  1. 无需线程上下文切换的开销

  2. 无需原子操作锁定及同步的开销

  3. 方便切换控制流,简化编程模型

  4. 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理

缺点

  1. 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。

  2. 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

协程 2.7

协程:单线程里面不断切换这个单线程中的微进程,即通过代码来实现让一个线程中的更小进程来回切换,相对于多线程多进程可以节省线程切换的时间。

代码实现

协程在Python中使用yield生成器实现,每次执行到yield位置代码停止,返回一个数据,随后在别的地方可以接手这个数据后,代码恢复继续执行

# -*- coding: utf-8 -*-
# @Time    : 2018/6/23 0023 10:19
# @Author  : Langzi
# @Blog    : www.langzi.fun
# @File    : 协程.py
# @Software: PyCharm
import sys
import time
reload(sys)
sys.setdefaultencoding('utf-8')

def fun_1():
    while 1:
        n = yield 'FUN_1 执行完毕,切换到FUN_2'
        # 函数运行到yield会暂停函数执行,存储这个值。并且有next():调用这个值,与send():外部传入一个值
        if not n:
            return
        time.sleep(1)
        print 'FUN_1 函数开始执行'

def fun_2(t):
    t.next()
    while 1:
        print '-'*20
        print 'FUN_2 函数开始执行'
        time.sleep(1)
        ret = t.send('over')
        print ret
    t.close()

if __name__ == '__main__':
    n = fun_1()
    fun_2(n)

可以看到,没有使用多线程处理,依然在两个函数中不断切换循环。

总结一下:

1. 第一个生产者函数中,使用yield,后面的代码暂时不会执行
2. 第一个函数执行到yield后,程序执行第二个函数,首先接受参数t,调用yield的下一个值,t.next()
3. 然后第二个函数继续执行,执行完后给第一个函数发送一些数据,ret=t.send(None),其中ret就是第一个函数中yield的值
4. 最后关闭,t.close()
5. 把第一个函数的运行结果(其实就是当执行到yield的值)传递给第二个函数,第二个函数继续执行,然后把返回值继续传递给第一个函数。

协程 3.5

在Python3中新增asyncio库,在 3.5+ 版本中, asyncio 有两样语法非常重要, async, await. 弄懂了它们是如何协同工作的, 我们就完全能发挥出这个库的功能了。

基本用法

我们要时刻记住,asyncio 不是多进程, 也不是多线程, 单单是一个线程, 但是是在 Python 的功能间切换着执行. 切换的点用 await 来标记, 使用async关键词将其变成协程方法, 比如 async def function():。其中,async 定义一个协程,await 用来挂起阻塞方法的执行。

概念

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

代码演示

先看看不是异步的

# 不是异步的
import time
def job(t):
    print('Start job ', t)
    time.sleep(t)               # wait for "t" seconds
    print('Job ', t, ' takes ', t, ' s')
def main():
    [job(t) for t in range(1, 3)]
t1 = time.time()
main()
print("NO async total time : ", time.time() - t1)

"""
Start job  1
Job  1  takes  1  s
Start job  2
Job  2  takes  2  s
NO async total time :  3.008603096008301

从上面可以看出, 我们的 job 是按顺序执行的, 必须执行完 job 1 才能开始执行 job 2, 而且 job 1 需要1秒的执行时间, 而 job 2 需要2秒. 所以总时间是 3 秒多. 而如果我们使用 asyncio 的形式, job 1 在等待 time.sleep(t) 结束的时候, 比如是等待一个网页的下载成功, 在这个地方是可以切换给 job 2, 让它开始执行.

然后是异步的

import asyncio
async def job(t):                   # async 形式的功能
    print('Start job ', t)
    await asyncio.sleep(t)          # 等待 "t" 秒, 期间切换其他任务
    print('Job ', t, ' takes ', t, ' s')
async def main(loop):                       # async 形式的功能
    tasks = [
    loop.create_task(job(t)) for t in range(1, 3)
    ]                                       # 创建任务, 但是不执行
    await asyncio.wait(tasks)               # 执行并等待所有任务完成
t1 = time.time()
loop = asyncio.get_event_loop()             # 建立 loop
loop.run_until_complete(main(loop))         # 执行 loop,并且等待所有任务结束
loop.close()                                # 关闭 loop
print("Async total time : ", time.time() - t1)
"""
Start job  1
Start job  2
Job  1  takes  1  s
Job  2  takes  2  s
Async total time :  2.001495838165283
"""

从结果可以看出, 我们没有等待 job 1 的结束才开始 job 2, 而是 job 1 触发了 await 的时候就切换到了 job 2 了. 这时, job 1 和 job 2 同时在等待 await asyncio.sleep(t), 所以最终的程序完成时间, 取决于等待最长的 t, 也就是 2秒. 这和上面用普通形式的代码相比(3秒), 的确快了很多.由于协程对象不能直接运行,在注册事件循环的时候,其实是run_until_complete方法将协程包装成为了一个任务(task)对象。所谓task对象是Future类的子类,保存了协程运行后的状态,用于未来获取协程的结果。

简单的例子:

import asyncio
import requests
async def scan(url):
    r = requests.get(url).status_code
    return r

task = asyncio.ensure_future(scan('http://www.langzi.fun'))
loop = asyncio.get_event_loop()
loop.run_until_complete(task)
print(task.result())

调用协程有好几种方法,这里就只看我这种即可,主要是后面三行。把任务赋值给task,然后loop为申请调度(这么理解),然后执行。因为requests这个库是同步堵塞的,所以没办法变成异步执行,这个时候学学aiohttp,一个唯一有可能在异步中取代requests的库。

绑定回调

就是让第一个函数执行后,执行的结果传递给第二个函数继续执行

例子:

import asyncio
import requests

async def request():
    url = 'https://www.baidu.com'
    status = requests.get(url)
    return status

def callback(task):
    print('Status:', task.result())

coroutine = request()
task = asyncio.ensure_future(coroutine)
task.add_done_callback(callback)
print('Task:', task)
loop = asyncio.get_event_loop()
loop.run_until_complete(task)
print('Task:', task)

在这里我们定义了一个 request() 方法,请求了百度,返回状态码,但是这个方法里面我们没有任何 print() 语句。随后我们定义了一个 callback() 方法,这个方法接收一个参数,是 task 对象,然后调用 print() 方法打印了 task 对象的结果。这样我们就定义好了一个 coroutine 对象和一个回调方法,我们现在希望的效果是,当 coroutine 对象执行完毕之后,就去执行声明的 callback() 方法。

那么它们二者怎样关联起来呢?很简单,只需要调用 add_done_callback() 方法即可,我们将 callback() 方法传递给了封装好的 task 对象,这样当 task 执行完毕之后就可以调用 callback() 方法了,同时 task 对象还会作为参数传递给 callback() 方法,调用 task 对象的 result() 方法就可以获取返回结果了。

多任务协程

就是把所有的任务加载到一个列表中,然后依次执行

上面的例子我们只执行了一次请求,如果我们想执行多次请求应该怎么办呢?我们可以定义一个 task 列表,然后使用 asyncio 的 wait() 方法即可执行,看下面的例子:

import asyncio
import requests

async def request():
    url = 'https://www.baidu.com'
    status = requests.get(url)
    return status

tasks = [asyncio.ensure_future(request()) for _ in range(5)]
print('Tasks:', tasks)

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

for task in tasks:
    print('Task Result:', task.result())

这里我们使用一个 for 循环创建了五个 task,组成了一个列表,然后把这个列表首先传递给了 asyncio 的 wait() 方法,然后再将其注册到时间循环中,就可以发起五个任务了。最后我们再将任务的运行结果输出出来,运行结果如下:

Tasks: [<Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>]
Task Result: <Response [200]>
Task Result: <Response [200]>
Task Result: <Response [200]>
Task Result: <Response [200]>
Task Result: <Response [200]>

多任务协程实现

上面的多任务协程执行了,但是是依次执行的

举例子测试,访问博客测试速度

import asyncio
import requests
import time

start = time.time()

async def request():
    url = 'http://www.langzi.fun'
    print('Waiting for', url)
    response = requests.get(url)
    print('Get response from', url, 'Result:', response.text)

tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

end = time.time()
print('Cost time:', end - start)

这个和上面一样,只是把所有的任务量加载一个tasks列表中罢了,并没有异步执行,但是不要慌,继续看代码

import asyncio
import aiohttp
import time

start = time.time()

async def get(url):
    session = aiohttp.ClientSession()
    response = await session.get(url)
    result = await response.text()
    session.close()
    return result

async def request():
    url = 'http://www.langzi.fun'
    print('Waiting for', url)
    result = await get(url)
    print('Get response from', url, 'Result:', result)

tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

end = time.time()
print('Cost time:', end - start)

为什么使用aiohttp呢?在之前就说过requests这个库是堵塞的,并不支持异步,而aiohttp是支持异步的网络请求的库。

协程嵌套

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

import asyncio

async def myfun(i):
    print('start {}th'.format(i))
    await asyncio.sleep(1)
    print('finish {}th'.format(i))

loop = asyncio.get_event_loop()
myfun_list = [asyncio.ensure_future(myfun(i)) for i in range(10)]
loop.run_until_complete(asyncio.wait(myfun_list))

这种用法和上面一种的不同在于后面调用的是asyncio.gather还是asyncio.wait,当前看成完全等价即可,所以平时使用用上面哪种都可以。

上面是最常看到的两种使用方式,这里列出来保证读者在看其他文章时不会发蒙。

另外,二者其实是有细微差别的

  • gather更擅长于将函数聚合在一起

  • wait更擅长筛选运行状况

    import asyncio
    import time
    now = lambda: time.time()
    async def do_some_work(x):
    print('Waiting: ', x)
    await asyncio.sleep(x)
    return ‘Done after {}s’.format(x)
    async def main():
    coroutine1 = do_some_work(1)
    coroutine2 = do_some_work(2)
    coroutine3 = do_some_work(4)
    tasks = [
    asyncio.ensure_future(coroutine1),
    asyncio.ensure_future(coroutine2),
    asyncio.ensure_future(coroutine3)
    ]
    dones, pendings = await asyncio.wait(tasks)

      for task in dones:
          print('Task ret: ', task.result())
    

    start = now()

    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

    print('TIME: ', now() - start)

如果使用的是 asyncio.gather创建协程对象,那么await的返回值就是协程运行的结果。

    results = await asyncio.gather(*tasks)

    for result in results:
        print('Task ret: ', result)

不在main协程函数里处理结果,直接返回await的内容,那么最外层的run_until_complete将会返回main协程的结果。

async def main():
    coroutine1 = do_some_work(1)
    coroutine2 = do_some_work(2)
    coroutine3 = do_some_work(2)

    tasks = [
        asyncio.ensure_future(coroutine1),
        asyncio.ensure_future(coroutine2),
        asyncio.ensure_future(coroutine3)
    ]

    return await asyncio.gather(*tasks)

start = now()

loop = asyncio.get_event_loop()
results = loop.run_until_complete(main())

for result in results:
    print('Task ret: ', result)

或者返回使用asyncio.wait方式挂起协程。

async def main():
    coroutine1 = do_some_work(1)
    coroutine2 = do_some_work(2)
    coroutine3 = do_some_work(4)

    tasks = [
        asyncio.ensure_future(coroutine1),
        asyncio.ensure_future(coroutine2),
        asyncio.ensure_future(coroutine3)
    ]

    return await asyncio.wait(tasks)

start = now()

loop = asyncio.get_event_loop()
done, pending = loop.run_until_complete(main())

for task in done:
    print('Task ret: ', task.result())

也可以使用asyncio的as_completed方法

async def main():
    coroutine1 = do_some_work(1)
    coroutine2 = do_some_work(2)
    coroutine3 = do_some_work(4)

    tasks = [
        asyncio.ensure_future(coroutine1),
        asyncio.ensure_future(coroutine2),
        asyncio.ensure_future(coroutine3)
    ]
    for task in asyncio.as_completed(tasks):
        result = await task
        print('Task ret: {}'.format(result))

start = now()

loop = asyncio.get_event_loop()
done = loop.run_until_complete(main())
print('TIME: ', now() - start)

由此可见,协程的调用和组合十分灵活,尤其是对于结果的处理,如何返回,如何挂起,需要逐渐积累经验和前瞻的设计。

协程停止

上面见识了协程的几种常用的用法,都是协程围绕着事件循环进行的操作。future对象有几个状态:

  • Pending
  • Running
  • Done
  • Cancelled

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

import asyncio

import time

now = lambda: time.time()

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

    await asyncio.sleep(x)
    return 'Done after {}s'.format(x)

coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(2)

tasks = [
    asyncio.ensure_future(coroutine1),
    asyncio.ensure_future(coroutine2),
    asyncio.ensure_future(coroutine3)
]

start = now()

loop = asyncio.get_event_loop()
try:
    loop.run_until_complete(asyncio.wait(tasks))
except KeyboardInterrupt as e:
    print(asyncio.Task.all_tasks())
    for task in asyncio.Task.all_tasks():
        print(task.cancel())
    loop.stop()
    loop.run_forever()
finally:
    loop.close()

print('TIME: ', now() - start)

启动事件循环之后,马上ctrl+c,会触发run_until_complete的执行异常 KeyBorardInterrupt。然后通过循环asyncio.Task取消future。可以看到输出如下:

Waiting:  1
Waiting:  2
Waiting:  2
{<Task pending coro=<do_some_work() running at /Users/ghost/Rsj217/python3.6/async/async-main.py:18> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x101230648>()]> cb=[_wait.<locals>._on_completion() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:374]>, <Task pending coro=<do_some_work() running at /Users/ghost/Rsj217/python3.6/async/async-main.py:18> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x1032b10a8>()]> cb=[_wait.<locals>._on_completion() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:374]>, <Task pending coro=<wait() running at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:307> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x103317d38>()]> cb=[_run_until_complete_cb() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/base_events.py:176]>, <Task pending coro=<do_some_work() running at /Users/ghost/Rsj217/python3.6/async/async-main.py:18> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x103317be8>()]> cb=[_wait.<locals>._on_completion() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:374]>}
True
True
True
True
TIME:  0.8858370780944824

True表示cannel成功,loop stop之后还需要再次开启事件循环,最后在close,不然还会抛出异常:

Task was destroyed but it is pending!
task: <Task pending coro=<do_some_work() done,

循环task,逐个cancel是一种方案,可是正如上面我们把task的列表封装在main函数中,main函数外进行事件循环的调用。这个时候,main相当于最外出的一个task,那么处理包装的main函数即可。

import asyncio
import time
now = lambda: time.time()

async def do_some_work(x):
    print('Waiting: ', x)
    await asyncio.sleep(x)
    return 'Done after {}s'.format(x)
async def main():
    coroutine1 = do_some_work(1)
    coroutine2 = do_some_work(2)
    coroutine3 = do_some_work(2)

    tasks = [
        asyncio.ensure_future(coroutine1),
        asyncio.ensure_future(coroutine2),
        asyncio.ensure_future(coroutine3)
    ]
    done, pending = await asyncio.wait(tasks)
    for task in done:
        print('Task ret: ', task.result())

start = now()

loop = asyncio.get_event_loop()
task = asyncio.ensure_future(main())
try:
    loop.run_until_complete(task)
except KeyboardInterrupt as e:
    print(asyncio.Task.all_tasks())
    print(asyncio.gather(*asyncio.Task.all_tasks()).cancel())
    loop.stop()
    loop.run_forever()
finally:
    loop.close()

不同线程的事件循环

很多时候,我们的事件循环用于注册协程,而有的协程需要动态的添加到事件循环中。一个简单的方式就是使用多线程。当前线程创建一个事件循环,然后在新建一个线程,在新线程中启动事件循环。当前线程不会被block。

from threading import Thread

def start_loop(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()

def more_work(x):
    print('More work {}'.format(x))
    time.sleep(x)
    print('Finished more work {}'.format(x))

start = now()
new_loop = asyncio.new_event_loop()
t = Thread(target=start_loop, args=(new_loop,))
t.start()
print('TIME: {}'.format(time.time() - start))

new_loop.call_soon_threadsafe(more_work, 6)
new_loop.call_soon_threadsafe(more_work, 3)

启动上述代码之后,当前线程不会被block,新线程中会按照顺序执行call_soon_threadsafe方法注册的more_work方法,后者因为time.sleep操作是同步阻塞的,因此运行完毕more_work需要大致6 + 3

新线程协程

def start_loop(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()

async def do_some_work(x):
    print('Waiting {}'.format(x))
    await asyncio.sleep(x)
    print('Done after {}s'.format(x))

def more_work(x):
    print('More work {}'.format(x))
    time.sleep(x)
    print('Finished more work {}'.format(x))

start = now()
new_loop = asyncio.new_event_loop()
t = Thread(target=start_loop, args=(new_loop,))
t.start()
print('TIME: {}'.format(time.time() - start))

asyncio.run_coroutine_threadsafe(do_some_work(6), new_loop)
asyncio.run_coroutine_threadsafe(do_some_work(4), new_loop)

上述的例子,主线程中创建一个new_loop,然后在另外的子线程中开启一个无限事件循环。主线程通过run_coroutine_threadsafe新注册协程对象。这样就能在子线程中进行事件循环的并发操作,同时主线程又不会被block。一共执行的时间大概在6s左右。

骚方法

requests实现异步爬虫一

如同前面介绍如何在asyncio中使用requests模块一样,如果想在asyncio中使用其他阻塞函数,该怎么实现呢?虽然目前有异步函数支持asyncio,但实际问题是大部分IO模块还不支持asyncio。 阻塞函数(例如io读写,requests网络请求)阻塞了客户代码与asycio事件循环的唯一线程,因此在执行调用时,整个应用程序都会冻结。

解决方案:

这个问题的解决方法是使用事件循环对象的run_in_executor方法。asyncio的事件循环在背后维护着一个ThreadPoolExecutor对象,我们可以调用run_in_executor方法,把可调用对象发给它执行,即可以通过run_in_executor方法来新建一个线程来执行耗时函数。

run_in_executor方法

AbstractEventLoop.run_in_executor(executor, func, *args)
  • executor 参数应该是一个 Executor 实例。如果为 None,则使用默认 executor。
  • func 就是要执行的函数
  • args 就是传递给 func 的参数

实际例子(使用time.sleep()):

import asyncio
import time
async def run(url):
    print("start ",url)
    loop = asyncio.get_event_loop()
    try:
        await loop.run_in_executor(None,time.sleep,1)
    except Exception as e:
        print(e)
    print("stop ",url)
url_list = ["https://thief.one","https://home.nmask.cn","https://movie.nmask.cn","https://tool.nmask.cn"]
tasks = [asyncio.ensure_future(run(url)) for url in url_list]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

说明:有了run_in_executor方法,我们就可以使用之前熟悉的模块创建协程并发了,而不需要使用特定的模块进行IO异步开发。

requests实现异步爬虫二

与之前学过的多线程、多进程相比,asyncio模块有一个非常大的不同:传入的函数不是随心所欲

  • 比如我们把上面myfun函数中的sleep换成time.sleep(1),运行时则不是异步的,而是同步,共等待了10秒

  • 如果我换一个myfun,比如换成下面这个使用request抓取网页的函数

    import asyncio
    import requests
    from bs4 import BeautifulSoup

    async def get_title(a):
    url = ‘https://movie.douban.com/top250?start={}&filter=’.format(a*25)
    r = requests.get(url)
    soup = BeautifulSoup(r.content, ‘html.parser’)
    lis = soup.find(‘ol’, class_=‘grid_view’).find_all(‘li’)
    for li in lis:
    title = li.find(‘span’, class_=“title”).text
    print(title)

    loop = asyncio.get_event_loop()
    fun_list = (get_title(i) for i in range(10))
    loop.run_until_complete(asyncio.gather(*fun_list))

依然不会异步执行。

到这里我们就会想,是不是异步只对它自己定义的sleep(await asyncio.sleep(1))才能触发异步?

对于上述函数,asyncio库只能通过添加线程的方式实现异步,下面我们实现time.sleep时的异步

import asyncio
import time

def myfun(i):
    print('start {}th'.format(i))
    time.sleep(1)
    print('finish {}th'.format(i))

async def main():
    loop = asyncio.get_event_loop()
    futures = (
        loop.run_in_executor(
            None,
            myfun, 
            i)
        for i in range(10)
        )
    for result in await asyncio.gather(*futures):
        pass

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

上面run_in_executor其实开启了新的线程,再协调各个线程。调用过程比较复杂,只要当模板一样套用即可。

上面10次循环仍然不是一次性打印出来的,而是像分批次一样打印出来的。这是因为开启的线程不够多,如果想要实现一次打印,可以开启10个线程,代码如下

import concurrent.futures as cf # 多加一个模块
import asyncio
import time

def myfun(i):
    print('start {}th'.format(i))
    time.sleep(1)
    print('finish {}th'.format(i))

async def main():
    with cf.ThreadPoolExecutor(max_workers = 10) as executor: # 设置10个线程
        loop = asyncio.get_event_loop()
        futures = (
            loop.run_in_executor(
                executor, # 按照10个线程来执行
                myfun, 
                i)
            for i in range(10)
            )
        for result in await asyncio.gather(*futures):
            pass

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

用这种方法实现requests异步爬虫代码如下

import concurrent.futures as cf
import asyncio
import requests
from bs4 import BeautifulSoup

def get_title(i):
    url = 'https://movie.douban.com/top250?start={}&filter='.format(i*25)
    r = requests.get(url)
    soup = BeautifulSoup(r.content, 'html.parser')
    lis = soup.find('ol', class_='grid_view').find_all('li')
    for li in lis:
        title = li.find('span', class_="title").text
        print(title)

async def main():
    with cf.ThreadPoolExecutor(max_workers = 10) as executor:
        loop = asyncio.get_event_loop()
        futures = (
            loop.run_in_executor(
                executor,
                get_title, 
                i)
            for i in range(10)
            )
        for result in await asyncio.gather(*futures):
            pass

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

uvloop,这个使用库可以有效的加速asyncio,本库基于libuv,也就是nodejs用的那个库。使用它也非常方便

import asyncio
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

没错就是2行代码,就可以提速asyncio。

与多进程的结合

既然异步协程和多进程对网络请求都有提升,那么为什么不把二者结合起来呢?在最新的 PyCon 2018 上,来自 Facebook 的 John Reese 介绍了 asyncio 和 multiprocessing 各自的特点,并开发了一个新的库,叫做 aiomultiprocess

这个库的安装方式是:

	pip3 install aiomultiprocess

需要 Python 3.6 及更高版本才可使用。

使用这个库,我们可以将上面的例子改写如下:

import asyncio
import aiohttp
import time
from aiomultiprocess import Pool

start = time.time()

async def get(url):
    session = aiohttp.ClientSession()
    response = await session.get(url)
    result = await response.text()
    session.close()
    return result

async def request():
    url = 'http://127.0.0.1:5000'
    urls = [url for _ in range(100)]
    async with Pool() as pool:
        result = await pool.map(get, urls)
        return result

coroutine = request()
task = asyncio.ensure_future(coroutine)
loop = asyncio.get_event_loop()
loop.run_until_complete(task)

end = time.time()
print('Cost time:', end - start)

这样就会同时使用多进程和异步协程进行请求,但在真实情况下,我们在做爬取的时候遇到的情况千变万化,一方面我们使用异步协程来防止阻塞,另一方面我们使用 multiprocessing 来利用多核成倍加速,节省时间其实还是非常可观的。

aiohttp基础用法

aiohttp分为服务器端和客户端,本文只介绍客户端。

案例:

import aiohttp
async def job(session):
    response = await session.get(URL)       # 等待并切换
    return str(response.url)


async def main(loop):
    async with aiohttp.ClientSession() as session:      # 官网推荐建立 Session 的形式
        tasks = [loop.create_task(job(session)) for _ in range(2)]
        finished, unfinished = await asyncio.wait(tasks)
        all_results = [r.result() for r in finished]    # 获取所有结果
        print(all_results)

t1 = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(main(loop))
loop.close()
print("Async total time:", time.time() - t1)

"""
['https://morvanzhou.github.io/', 'https://morvanzhou.github.io/']
Async total time: 0.11447715759277344
"""

我们刚刚创建了一个 Session, 这是官网推荐的方式, 但是我觉得也可以直接用 request 形式, 细节请参考官方说明. 如果要获取网页返回的结果, 我们可以在 job() 中 return 个结果出来, 然后再在 finished, unfinished = await asyncio.wait(tasks) 收集完成的结果, 这里它会返回完成的和没完成的, 我们关心的都是完成的, 而且 await 也确实是等待都完成了才返回. 真正的结果被存放在了 result() 里面.

aiohttp安装

pip3 install aiohttp

基本请求用法

async with aiohttp.get('https://github.com') as r:
        await r.text()

其中r.text(), 可以在括号中指定解码方式,编码方式,例如

await resp.text(encoding='windows-1251')

或者也可以选择不编码,适合读取图像等,是无法编码的

await resp.read()

发起一个session请求

首先是导入aiohttp模块:

import aiohttp

然后我们试着获取一个web源码,这里以GitHub的公共Time-line页面为例:

async with aiohttp.ClientSession() as session:
    async with session.get('https://api.github.com/events') as resp:
        print(resp.status)
        print(await resp.text())

上面的代码中,我们创建了一个 ClientSession 对象命名为session,然后通过session的get方法得到一个 ClientResponse 对象,命名为resp,get方法中传入了一个必须的参数url,就是要获得源码的http url。至此便通过协程完成了一个异步IO的get请求。
有get请求当然有post请求,并且post请求也是一个协程:

session.post('http://httpbin.org/post', data=b'data')

用法和get是一样的,区别是post需要一个额外的参数data,即是需要post的数据。
除了get和post请求外,其他http的操作方法也是一样的:

session.put('http://httpbin.org/put', data=b'data')
session.delete('http://httpbin.org/delete')
session.head('http://httpbin.org/get')
session.options('http://httpbin.org/get')
session.patch('http://httpbin.org/patch', data=b'data')

小记:
不要为每次的连接都创建一次session,一般情况下只需要创建一个session,然后使用这个session执行所有的请求。

每个session对象,内部包含了一个连接池,并且将会保持连接和连接复用(默认开启)可以加快整体的性能。

在URL中传递参数

我们经常需要通过 get 在url中传递一些参数,参数将会作为url问号后面的一部分发给服务器。在aiohttp的请求中,允许以dict的形式来表示问号后的参数。举个例子,如果你想传递 key1=value1 key2=value2 到 httpbin.org/get 你可以使用下面的代码:

params = {'key1': 'value1', 'key2': 'value2'}
async with session.get('http://httpbin.org/get',
                       params=params) as resp:
                       assert resp.url == 'http://httpbin.org/get?key2=value2&key1=value1'

可以看到,代码正确的执行了,说明参数被正确的传递了进去。不管是一个参数两个参数,还是更多的参数,都可以通过这种方式来传递。除了这种方式之外,还有另外一个,使用一个 list 来传递(这种方式可以传递一些特殊的参数,例如下面两个key是相等的也可以正确传递):

params = [('key', 'value1'), ('key', 'value2')]
async with session.get('http://httpbin.org/get',
                       params=params) as r:
    assert r.url == 'http://httpbin.org/get?key=value2&key=value1'

除了上面两种,我们也可以直接通过传递字符串作为参数来传递,但是需要注意,通过字符串传递的特殊字符不会被编码:

async with session.get('http://httpbin.org/get',
                       params='key=value+1') as r:
        assert r.url == 'http://httpbin.org/get?key=value+1'

响应的内容

还是以GitHub的公共Time-line页面为例,我们可以获得页面响应的内容:

async with session.get('https://api.github.com/events') as resp:
	print(await resp.text())

运行之后,会打印出类似于如下的内容:

'[{"created_at":"2015-06-12T14:06:22Z","public":true,"actor":{...

resp的text方法,会自动将服务器端返回的内容进行解码–decode,当然我们也可以自定义编码方式:

await resp.text(encoding='gb2312')

除了text方法可以返回解码后的内容外,我们也可以得到类型是字节的内容:

print(await resp.read())

运行的结果是:

b'[{"created_at":"2015-06-12T14:06:22Z","public":true,"actor":{...

gzip和deflate转换编码已经为你自动解码。

小记:

text(),read()方法是把整个响应体读入内存,如果你是获取大量的数据,请考虑使用”字节流“(streaming response)

特殊响应内容:json

如果我们获取的页面的响应内容是json,aiohttp内置了更好的方法来处理json:

async with session.get('https://api.github.com/events') as resp:
    print(await resp.json())

如果因为某种原因而导致resp.json()解析json失败,例如返回不是json字符串等等,那么resp.json()将抛出一个错误,也可以给json()方法指定一个解码方式:

print(await resp.json(
encoding='gb2312')) 

或者传递一个函数进去:

print(await resp.json( lambda(x:x.replace('a','b')) ))

以字节流的方式读取响应内容

虽然json(),text(),read()很方便的能把响应的数据读入到内存,但是我们仍然应该谨慎的使用它们,因为它们是把整个的响应体全部读入了内存。即使你只是想下载几个字节大小的文件,但这些方法却将在内存中加载所有的数据。所以我们可以通过控制字节数来控制读入内存的响应内容:

async with session.get('https://api.github.com/events') as resp:
    await resp.content.read(10) #读取前10个字节

一般地,我们应该使用以下的模式来把读取的字节流保存到文件中:

with open(filename, 'wb') as fd:
    while True:
        chunk = await resp.content.read(chunk_size)
        if not chunk:
            break
        fd.write(chunk)

自定义请求头

如果你想添加请求头,可以像get添加参数那样以dict的形式,作为get或者post的参数进行请求:

import json
url = 'https://api.github.com/some/endpoint'
payload = {'some': 'data'}
headers = {'content-type': 'application/json'}

await session.post(url,
                   data=json.dumps(payload),
                   headers=headers)

自定义Cookie

给服务器发送cookie,可以通过给 ClientSession 传递一个cookie参数:

url = 'http://httpbin.org/cookies'
cookies = {'cookies_are': 'working'}
async with ClientSession(cookies=cookies) as session:
    async with session.get(url) as resp:
        assert await resp.json() == {
           "cookies": {"cookies_are": "working"}}

可直接访问链接 “httpbin.org/cookies”查看当前cookie,访问session中的cookie请见第10节。

post数据的几种方式

(1)模拟表单post数据

payload = {'key1': 'value1', 'key2': 'value2'}
async with session.post('http://httpbin.org/post',data=payload) as resp:
	print(await resp.text())

注意:data=dict的方式post的数据将被转码,和form提交数据是一样的作用,如果你不想被转码,可以直接以字符串的形式 data=str 提交,这样就不会被转码。

(2)post json

import json
url = 'https://api.github.com/some/endpoint'
payload = {'some': 'data'}

async with session.post(url, data=json.dumps(payload)) as resp:
	...

其实json.dumps(payload)返回的也是一个字符串,只不过这个字符串可以被识别为json格式

(3)post 小文件

url = 'http://httpbin.org/post'
files = {'file': open('report.xls', 'rb')}

await session.post(url, data=files)
可以设置好文件名和content-type:
url = 'http://httpbin.org/post'
data = FormData()
data.add_field('file',
               open('report.xls', 'rb'),
               filename='report.xls',
               content_type='application/vnd.ms-excel')

await session.post(url, data=data)

如果将文件对象设置为数据参数,aiohttp将自动以字节流的形式发送给服务器。

(4)post 大文件

aiohttp支持多种类型的文件以流媒体的形式上传,所以我们可以在文件未读入内存的情况下发送大文件。

@aiohttp.streamer
def file_sender(writer, file_name=None):
    with open(file_name, 'rb') as f:
        chunk = f.read(2**16)
        while chunk:
            yield from writer.write(chunk)
            chunk = f.read(2**16)

# Then you can use `file_sender` as a data provider:

async with session.post('http://httpbin.org/post',data=file_sender(file_name='huge_file')) as resp:
    print(await resp.text())

同时我们可以从一个url获取文件后,直接post给另一个url,并计算hash值:

async def feed_stream(resp, stream):
    h = hashlib.sha256()

    while True:
        chunk = await resp.content.readany()
        if not chunk:
            break
        h.update(chunk)
        stream.feed_data(chunk)

    return h.hexdigest()

resp = session.get('http://httpbin.org/post')
stream = StreamReader()
loop.create_task(session.post('http://httpbin.org/post', data=stream))
file_hash = await feed_stream(resp, stream)

因为响应内容类型是StreamReader,所以可以把get和post连接起来,同时进行post和get:

r = await session.get('http://python.org')
await session.post('http://httpbin.org/post',data=r.content)

(5)post预压缩数据

在通过aiohttp发送前就已经压缩的数据, 调用压缩函数的函数名(通常是deflate 或 zlib)作为content-encoding的值:

async def my_coroutine(session, headers, my_data):
    data = zlib.compress(my_data)
    headers = {'Content-Encoding': 'deflate'}
    async with session.post('http://httpbin.org/post',
                            data=data,
                            headers=headers)
        pass

keep-alive, 连接池,共享cookie

ClientSession 用于在多个连接之间共享cookie:

async with aiohttp.ClientSession() as session:
    await session.get(
        'http://httpbin.org/cookies/set?my_cookie=my_value')
    filtered = session.cookie_jar.filter_cookies('http://httpbin.org')
    assert filtered['my_cookie'].value == 'my_value'
    async with session.get('http://httpbin.org/cookies') as r:
        json_body = await r.json()
        assert json_body['cookies']['my_cookie'] == 'my_value'

也可以为所有的连接设置共同的请求头:

async with aiohttp.ClientSession(
    headers={"Authorization": "Basic bG9naW46cGFzcw=="}) as session:
    async with session.get("http://httpbin.org/headers") as r:
        json_body = await r.json()
        assert json_body['headers']['Authorization'] == \
            'Basic bG9naW46cGFzcw=='

ClientSession 还支持 keep-alive连接和连接池(connection pooling)

cookie安全性

默认ClientSession使用的是严格模式的 aiohttp.CookieJar. RFC 2109,明确的禁止接受url和ip地址产生的cookie,只能接受 DNS 解析IP产生的cookie。可以通过设置aiohttp.CookieJar 的 unsafe=True 来配置:

jar = aiohttp.CookieJar(unsafe=True)
session = aiohttp.ClientSession(cookie_jar=jar)

控制同时连接的数量(连接池)

也可以理解为同时请求的数量,为了限制同时打开的连接数量,我们可以将限制参数传递给连接器:

conn = aiohttp.TCPConnector(limit=30)#同时最大进行连接的连接数为30,默认是100,limit=0的时候是无限制

限制同时打开限制同时打开连接到同一端点的数量((host, port, is_ssl) 三的倍数),可以通过设置 limit_per_host 参数:

conn = aiohttp.TCPConnector(limit_per_host=30)#默认是0

自定义域名解析

我们可以指定域名服务器的 IP 对我们提供的get或post的url进行解析:

from aiohttp.resolver import AsyncResolver
resolver = AsyncResolver(nameservers=["8.8.8.8", "8.8.4.4"])
conn = aiohttp.TCPConnector(resolver=resolver)

设置代理

aiohttp支持使用代理来访问网页:

async with aiohttp.ClientSession() as session:
    async with session.get("http://python.org",
                           proxy="http://some.proxy.com") as resp:
        print(resp.status)

当然也支持需要授权的页面:

async with aiohttp.ClientSession() as session:
    proxy_auth = aiohttp.BasicAuth('user', 'pass')
    async with session.get("http://python.org",proxy="http://some.proxy.com",proxy_auth=proxy_auth) as resp:
        print(resp.status)

或者通过这种方式来验证授权:

session.get("http://python.org",proxy="http://user:pass@some.proxy.com")

响应状态码 response status code

可以通过 resp.status来检查状态码是不是200:

async with session.get('http://httpbin.org/get') as resp:
    assert resp.status == 200

响应头

我们可以直接使用 resp.headers 来查看响应头,得到的值类型是一个dict:

>>> resp.headers
{'ACCESS-CONTROL-ALLOW-ORIGIN': '*',
 'CONTENT-TYPE': 'application/json',
 'DATE': 'Tue, 15 Jul 2014 16:49:51 GMT',
 'SERVER': 'gunicorn/18.0',
 'CONTENT-LENGTH': '331',
 'CONNECTION': 'keep-alive'}

或者我们可以查看原生的响应头:

>>> resp.raw_headers
((b'SERVER', b'nginx'),
 (b'DATE', b'Sat, 09 Jan 2016 20:28:40 GMT'),
 (b'CONTENT-TYPE', b'text/html; charset=utf-8'),
 (b'CONTENT-LENGTH', b'12150'),
 (b'CONNECTION', b'keep-alive'))

重定向的响应头

如果一个请求被重定向了,我们依然可以查看被重定向之前的响应头信息:

>>> resp = await session.get('http://example.com/some/redirect/')
>>> resp
<ClientResponse(http://example.com/some/other/url/) [200]>
>>> resp.history
(<ClientResponse(http://example.com/some/redirect/) [301]>,)

超时处理

默认的IO操作都有5分钟的响应时间 我们可以通过 timeout 进行重写:

async with session.get('https://github.com', timeout=60) as r:
    ...

Python协程

Python3.5协程学习研究

asyncio:高性能异步模块使用介绍

爬虫速度太慢?来试试用异步协程提速吧!

对Python并发编程的思考

aiohttp 简易使用教程

aiohttp 中文文档

python异步asyncio模块的使用

Asyncio 使用经验

Python-aiohttp百万并发

欢迎关注公众号:【安全研发】获取更多相关工具,课程,资料分享哦~
在这里插入图片描述

Python协程在数据处理中的应用:异步ETL实战案例 在当今大数据时代,数据处理的效率至关重要。ETL是数据处理中常见的操作,传统的ETL方式在处理大量数据时可能会遇到性能瓶颈。Python协程提供了一种异步编程的方式,可以显著提高数据处理的效率。本文的目的就是介绍如何使用Python协程来实现异步ETL,范围涵盖了从基本概念的讲解到实际项目的开发。本文首先介绍相关的术语和核心概念,通过故事引入让读者更容易理解。接着讲解核心概念之间的关系,并给出原理和架构的示意图。然后通过Python代码详细阐述核心算法原理和具体操作步骤,结合数学模型和公式进行说明。 阅读详情

相关推荐

深入理解Python生成器协程

本文深入探讨了Python中的生成器和协程机制。通过分析Python-cheatsheet文档中关于yield、yield from、生成器类型检查以及上下文管理器等高级特性的内容,我们揭示了这些特性在实际编程中的应用和内在原理。此外,还探讨了如何使用yield from进行迭代器委托以及如何将生成器协程结合,实现异步编程。

weixin_35728286的博客 738

python标准库、threading、multiprocessing、aiomultiprocess、进程/线程 同步、queue(队列)

multiprocessing 包提供了本地和远程并发操作,通过使用子进程而非线程有效地绕过了全局解释器锁。 因此,multiprocessing 模块允许程序员充分利用给定机器上的多个处理器。

freeking101的博客 2万+

aiomultiprocess:将现代的Python代码库提升到新的性能水平

多进程 将现代的Python代码库提升到新的性能水平。 AsyncIO和多处理本身是有用的,但有局限性:AsyncIO仍然不能超过GIL的速度,并且多处理一次只能处理一个任务。 但是,他们在一起可以充分发挥自己的真正潜力。 aiomultiprocess提供了一个简单的界面,同时在每个子进程上运行完整的AsyncIO事件循环,从而实现了Python应用程序从未有过的并发级别。 每个子进程可以一次执行多个协程,仅受工作量和可用内核数限制。 在几秒钟内收集成千上万的网络请求非常简单: async with Pool () as pool : results = await pool . map ( < coroutine> , < items> ) 安装 aiomultiprocess需要Python 3.6或更高版本。 您可以从PyPI安装它: $ p

探秘高效并发处理利器:`aiomultiprocess`

Python的世界中,我们常常需要处理大量的并发任务,以提高程序的执行效率。`aiomultiprocess`就是这样一款专为异步IO设计的多进程库,它结合了Python的`asyncio`框架和`multiprocessing`模块,使得开发者可以在异步环境下充分利用多核处理器的优势。让我们一起深入了解并体验这款项目的魅力。 ## 项目简介 `aiomultiprocess`(仓库链接: ...

gitblog_00049的博客 838

python3多进程包_一个异步多进程pythonaiomultiprocess

aiomultiprocess是一个异步多进程的python库,其依赖于aiohttp和asyncio两个库。aiohttp是一个基于asyncio异步http客户端和服务器。asyncio 是用来编写 并发 代码的库,使用 async/await 语法。asyncio 被用作多个提供高性能 Python 异步框架的基础,包括网络和网站服务,数据库连接库,分布式任务队列等等。On their o...

weixin_39982580的博客 1239

aiohttpaiomultiprocess结合使用

这是之前听大神讲异步的时候看到的一篇教程,后来又查了一些资料,汇总了一下,先记下来,后续用到了再深入研究。

Dahlin哥's 博客 2452

Python Asyncio 所有异步协程库用法详解

title: Asyncio并发编程 copyright: true top: 0 date: 2019-04-03 14:09:24 tags: Asyncio categories: Python高阶笔记 permalink: password: keywords: description: Python高并发框架,进一步解读asyncio的语法功能,同时对支持异步的web网络请求包aiohttp功能做介绍。 我喜欢铁路,你沿着铁路走,在尽头肯定会找到一座城市,或者其他什么有人的地方。不像鸟飞在空中.

浪子燕青的博客 2478

掌握Python异步IO利器:深入解读Asyncio

异步IO是一种编程模型,允许程序在执行I/O操作时不必等待其完成。传统的同步IO相比,异步IO可以在进行一个I/O操作的同时继续执行其他任务。这种机制特别适用于网络请求频繁或需要大量I/O操作的应用场景。AsyncioPython标准库中提供的异步编程库,基于协程(coroutines)和事件循环(event loop)的概念。通过async/await关键字,它允许开发者编写异步函数和操作,然后通过一个事件循环在单个线程中调度和执行这些操作。

涛哥聊Python 1419

python协程使用场景_详细解读tornado协程(coroutine)原理

tornado中的协程是如何工作的协程定义Coroutines are computer program components that generalize subroutines for nonpreemptive multitasking, by allowing multiple entry points for suspending and resuming execution at c...

weixin_39946996的博客 487

python ico_Python协程asynico模块解读

为了搞清楚asynico模块的具体作用,那我们要先明白一些基本概念。一般在爬虫里面,为了加快速度,我们可以使用多进程、多线程、协程,这篇文章详细解读一下这三者的区别的区别。在一个主程序运行的时候,会有一个主进程产生,但是进程是不能处理任务的,要处理具体任务,例如发送请求,读写文件,它就要产生一个主线程帮它完成,而在任务处理的时候,这里继续以发送请求为例,很多时候请求发送出去了,但回复并不是及时的,...

weixin_39772388的博客 283

异步爬虫:利用异步协程抓取一部电影】

我们把目光转向wbdy. 目前该案例还是可以用的.我们想要抓取网上的视频资源就必须要了解我们的视频网站是如何工作的. 这里我用91看剧来做举例. 其他网站的原理是一样的.

weixin_50556117的博客 947

异步等待的 Python 协程

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_34072159的博客 190

python协程使用场景_python协程的那些事

python如何设置多进程(直通车)协程基本概念协程,又称微线程,纤程。英文名Coroutine。协程是一种用户态的轻量级线程。协程原理协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次...

weixin_39951396的博客 183

Asyncio子进程通信机制深度解读,揭开协程系统交互的神秘面纱

掌握高效Asyncio子进程管理,解决协程系统交互难题。本文深入解析异步子进程通信机制,涵盖创建、监控数据交换的核心方法,适用于高并发IO场景,提升程序响应速度资源利用率,值得收藏。

FuncInk的博客 585

探索异步GRPC新时代:aiogrpc深度解读应用

探索异步GRPC新时代:aiogrpc深度解读应用 在高速发展的云计算和微服务架构中,高效且可靠的通信框架变得至关重要。今天,我们带您深入了解一个专为Python设计的优秀开源项目——aiogrpc,它旨在释放异步编程的全部潜力,将GRPC的强大功能Pythonasyncio库无缝结合,开启高性能服务间通信的新篇章。 项目介绍 aiogrpc是针对google的gRPC框架的一个高效扩展,专...

gitblog_00594的博客 936

从同步到异步的跃迁,深度解读Python 3.5 async/await

掌握异步编程核心技能,本文带你深入Python 3.5 async/await 语法入门,解析协程原理事件循环机制。涵盖网络请求、文件读写等高并发应用场景,对比传统同步代码性能优势。助你写出更高效的Python程序,值得收藏。

FuncFun的博客 868

异步IO性能优化全解析,深度解读 aiohttp asyncio 协作机制

掌握Python异步编程进阶技巧,提升高并发IO性能。深入解析aiohttpasyncio协作机制,涵盖Web爬虫、API服务等应用场景,详解事件循环优化协程调度策略,显著降低响应延迟。性能优化全方案值得收藏。

DevPath的博客 893

asyncio异常处理全解析,深度解读协程中断错误传递机制

掌握asyncio异步任务的取消异常处理,深入解析协程中断机制错误传递路径。涵盖超时控制、Task.cancel()用法、CancelledError捕获等关键方法,适用于高并发网络爬虫服务开发。原理清晰、示例详实,值得收藏。

CompiTide的博客 680
上一篇: Lang_URL深度采集工具
下一篇: Python打包exe文件方法汇总【4种】
浪子燕青啦啦啦
博客等级 码龄10年 371粉丝 109原创
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浪子燕青啦啦啦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值