关于python:Python协程-异步编程asyncio-入门介绍

39次阅读

共计 5818 个字符,预计需要花费 15 分钟才能阅读完成。

本文首发于:行者 AI

在近期的编码工作过程中遇到了 async 和 await 装璜的函数,查问材料后理解到这种函数是基于协程的异步函数。这类编程形式称为异步编程,罕用在 IO 较频繁的零碎中,如:Tornado web 框架、文件下载、网络爬虫等利用。协程可能在 IO 等待时间就去切换执行其余工作,当 IO 操作完结后再主动回调,那么就会大大节俭资源并提供性能。接下来便简略的解说一下异步编程相干概念以及案例演示。

1. 协程简介

1.1 协程的含意及实现办法

协程(Coroutine),也能够被称为微线程,是一种用户态内的上下文切换技术。简而言之,其实就是通过一个线程实现代码块互相切换执行。例如:

def func1():
    print(1)
    ...   # 协程染指
    print(2)
    
def func2():
    print(3)
    ...   # 协程染指
    print(4)

func1()
func2()

上述代码是一般的函数定义和执行,按流程别离执行两个函数中的代码,并先后会输入:1、2、3、4。但如果染指协程技术那么就能够实现函数见代码切换执行,最终输出:1、3、2、4

在 Python 中有多种形式能够实现协程,例如:

  • greenlet,是一个第三方模块,用于实现协程代码(Gevent 协程就是基于 greenlet 实现);
  • yield,生成器,借助生成器的特点也能够实现协程代码;
  • asyncio,在 Python3.4 中引入的模块用于编写协程代码;
  • async & awiat,在 Python3.5 中引入的两个关键字,联合 asyncio 模块能够更不便的编写协程代码。

前两种实现形式较为老旧,所以重点关注前面的形式

规范库实现办法

asyncio 是 Python 3.4 版本引入的规范库,间接内置了对异步 IO 的反对。

import asyncio

@asyncio.coroutine
def func1():
    print(1)
    yield from asyncio.sleep(2)  # 遇到 IO 耗时操作,自动化切换到 tasks 中的其余工作
    print(2)

@asyncio.coroutine
def func2():
    print(3)
    yield from asyncio.sleep(2) # 遇到 IO 耗时操作,自动化切换到 tasks 中的其余工作
    print(4)

tasks = [asyncio.ensure_future( func1() ),
    asyncio.ensure_future(func2() )
]

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

关键字实现办法

async & await 关键字在 Python3.5 版本中正式引入,代替了asyncio.coroutine 装璜器,基于他编写的协程代码其实就是上一示例的加强版,让代码能够更加简便可读。


import asyncio

async def func1():
    print(1)
    await asyncio.sleep(2)  # 耗时操作  
    print(2)

async def func2():
    print(3)
    await asyncio.sleep(2)   # 耗时操作
    print(4)

tasks = [asyncio.ensure_future(func1()),
    asyncio.ensure_future(func2())
]

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

1.2 案例演示

例如:用代码实现下载 url_list 中的图片。

  • 形式一:同步编程实现
# requests 库仅反对同步的 http 网络申请
import requests

def download_image(url):
  print("开始下载:",url)
  # 发送网络申请,下载图片
  response = requests.get(url)
  # 图片保留到本地文件
  file_name = url.rsplit('_')[-1]
  with open(file_name, mode='wb') as file_object:
      file_object.write(response.content)
print("下载实现")


if __name__ == '__main__':
  url_list = [
      'https://www.1.jpg',
      'https://www.2.jpg',
      'https://www.3.jpg'
  ]
  for item in url_list:
      download_image(item)

输入:按程序发送申请,申请一次下载一张图片,如果每次下载破费 1s,实现工作须要 3s 以上。

  • 形式二:基于协程的程实现
# aiohttp 为反对异步编程的 http 申请库
import aiohttp
import asyncio

async def fetch(session, url):
  print("发送申请:", url)
  async with session.get(url, verify_ssl=False) as response:
      content = await response.content.read()
      file_name = url.rsplit('_')[-1]
      with open(file_name, mode='wb') as file_object:
          file_object.write(content)

async def main():
  async with aiohttp.ClientSession() as session:
      url_list = [
          'https://www.1.jpg',
          'https://www.2.jpg',
          'https://www.3.jpg'
      ]
      tasks = [asyncio.create_task(fetch(session, url)) for url in url_list]
      await asyncio.wait(tasks)


if __name__ == '__main__':
  asyncio.run(main())

输入:一次发送三个下载申请,同时下载,如果每次下载破费 1s,实现工作仅须要 1s 左右,第一种办法的耗时为第二种的三倍。

1.3 小结

协程能够让原来要应用异步 + 回调形式写的非人类代码,用看似同步的形式写进去。

2. 异步编程简介

2.1 同步和异步的区别

同步:循序渐进执行操作、申请
异步:无需期待上一步操作、申请实现,就开始下一步(每个操作依然有先后顺序)


目前 python 异步相干的支流技术是通过蕴含关键字 async&await 的 async 模块实现。

2.2 异步编程 - 事件循环

事件循环,能够把他当做是一个 while 循环,这个 while 循环在周期性的运行并执行一些工作,在特定条件下终止循环。

# 伪代码
工作列表 = [工作 1, 工作 2, 工作 3,...]
while True:
    可执行的工作列表,已实现的工作列表 = 去工作列表中查看所有的工作,将 '可执行' 和 '已实现' 的工作返回
    for 就绪工作 in 已准备就绪的工作列表:
        执行已就绪的工作
        
    for 已实现的工作 in 已实现的工作列表:
        在工作列表中移除 已实现的任
        
    如果 工作列表 中的工作都已实现,则终止循环

在编写程序时候能够通过如下代码来获取和创立事件循环。

# 形式一:import asyncio
# 生成或获取一个事件循环
loop = asyncio.get_event_loop()
# 将工作增加到事件循环中
loop.run_until_complete(工作)

# 形式二(python3.7 及以上版本反对):asyncio.run(工作)

2.3 异步编程 - 疾速上手

async 关键字

  • 协程函数:定义函数时候由 async 关键字装璜的函数 async def 函数名
  • 协程对象:执行协程函数 () 失去的协程对象。
# 协程函数
async def func():
    pass
# 协程对象
result = func()

留神:执行协程函数只会创立协程对象,函数外部代码不会执行。如果想要运行协程函数外部代码,必须要将协程对象交给事件循环来解决。

import asyncio 
async def func():
    print("执行协程函数外部代码!")
result = func()

# 调用办法 1:# loop = asyncio.get_event_loop()
# loop.run_until_complete(result)

# 调用办法 2:asyncio.run(result)

await 关键字

await + 可期待的对象(协程对象、Future、Task 对象 -> IO 期待),遇到 IO 操作挂起以后协程(工作),等 IO 操作实现之后再持续往下执行。以后协程挂起时,事件循环能够去执行其余协程(工作)。

import asyncio

async def others():
    print("start")
    await asyncio.sleep(2)
    print('end')
    return '返回值'

async def func():
    print("执行协程函数外部代码")
    # await 期待对象的值得到后果之后再持续向下走
    response = await others()
    print("IO 申请完结,后果为:", response)

asyncio.run(func() )

Task 对象

Task 对象的作用是在事件循环中增加多个工作,用于并发调度协程,通过 asyncio.create_task(协程对象) 的形式创立 Task 对象,这样能够让协程退出事件循环中期待被调度执行。

async def module_a():
    print("start module_a")
    await asyncio.sleep(2) # 模仿 module_a 的 io 操作
    print('end module_a')
    return 'module_a 实现'

async def module_b():
    print("start module_b")
    await asyncio.sleep(1) # 模仿 module_a 的 io 操作
    print('end module_b')
    return 'module_b 实现'  

task_list = [module_a(),
    module_b(),]

done,pending = asyncio.run(asyncio.wait(task_list) )
print(done)

2.4 案例演示

例如:用代码实现连贯并查询数据库的同时,下载一个 APK 文件到本地。

import asyncio
import aiomysql
import os
import aiofiles as aiofiles
from aiohttp import ClientSession

async def get_app():

    url = "http://www.123.apk"
    async with ClientSession() as session:
        # 网络 IO 申请,获取响应
        async with session.get(url)as res:
            if res.status == 200:
                print("下载胜利", res)
                # 磁盘 IO 申请,读取响应数据
                apk = await res.content.read()
                async  with  aiofiles.open("demo2.apk", "wb") as f:
                    # 磁盘 IO 申请,数据写入本地磁盘
                    await f.write(apk)
            else:
                print("下载失败")

async def excute_sql(sql):
    # 网络 IO 操作:连贯 MySQL
    conn = await aiomysql.connect(host='127.0.0.1', port=3306, user='root', password='123', db='mysql',)
    # 网络 IO 操作:创立 CURSOR
    cur = await conn.cursor()
    # 网络 IO 操作:执行 SQL
    await cur.execute(sql)
    # 网络 IO 操作:获取 SQL 后果
    result = await cur.fetchall()
    print(result)
    # 网络 IO 操作:敞开链接
    await cur.close()
    conn.close()

task_list = [get_app(), execute_sql(sql="SELECT Host,User FROM user")]
asyncio.run(asyncio.wait(task_list))

代码逻辑剖析:

【step1】asyncio.run()创立了事件循环。wait()办法将 task 工作列表退出到以后的事件循环中;(留神:必须先创立事件循环,后退出工作列表,否则会报错)

【step2】事件循环监听事件状态,开始执行代码,先执行列表中的 get_app() 办法,当代码执行到 async with session.get(url)as res: 时,遇到 await 关键字示意有 IO 耗时操作,线程会将该工作挂起在后盾执行,并切换到另外一个异步函数excute_sql()

【step3】当代码执行到 excute_sql() 的第一个 IO 耗时操作后,线程会反复先前的操作,将该工作挂起,去执行其余可执行代码。如果此时事件循环监听到 get_app() 中的第一 IO 耗时操作曾经执行实现,那么线程会切换到该办法第一个 IO 操作后的代码,并按程序执行直到遇上下一个 await 装璜的 IO 操作;如果事件循环监听到 excute_sql() 中的第一个 IO 操作先于 get_app() 的第一个 IO 操作实现,那么线程会继续执行 excute_sql 的后续代码;

【step4】线程会反复进行上述第 3 点中的步骤,直到代码全副执行实现,事件循环也会随之进行。

2.5 大节

一般来说 CPU 的耗时运算形式有:

计算密集型的操作:计算密集型工作的特点是要进行大量的计算、逻辑判断,耗费 CPU 资源,比方计算圆周率、对视频进行高清解码等等。

IO 密集型的操作:波及到网络、磁盘 IO 的工作都是 IO 密集型工作,这类工作的特点是 CPU 耗费很少,工作的大部分工夫都在期待 IO 操作实现(因为 IO 的速度远远低于 CPU 和内存的速度)。

异步编程基于协程实现,如果利用协程实现计算密集型操作,因为线程在上下文之间的来回切换总会经验相似于”计算“–>”保留“–>”创立新环境“的一系列操作,导致系统的整体性能反而会降落。所以异步编程并不适用于计算密集型的程序。然而在 IO 密集型操作汇总,协程在 IO 等待时间就去切换执行其余工作,当 IO 操作完结后再主动回调,那么就会大大节俭资源并提供性能。

正文完
 0