Python 异步编程之协程

2022-08-30 12:59:15

1.总则

       多进程可以实现真正的并行,但进程间无法进行直接通信且占用资源较多。多线程的使用代价相对多进程较小,但为了解决数据安全问题引入了锁的机制。这又使得多线程并发度降低,同时,使用锁还可能造成死锁。

        在此背景下,协程出现了。协程是单线程,是任务级的切换。相比线程和进程切换,代价小得多,并发性能也很高。

2.实现原理

上面提到,协程是任务级的切换,具体地说就是函数级的切换。这句话展开有两个要点。①要有一个负责切换函数执行的循环;②函数要能暂停和重启。函数要实现暂停和重启,就要引入生成器的概念。

(1)生成器——函数暂停和重启

简单讲,函数内部使用了关键字yield就是生成器。

def gen_fun():
    yield 1
    yield 2


g = gen_fun()
print(next(g))
print(next(g))


# 输出:1
       2

生成器函数和普通函数相比,首先是使用了yield。yield和return都可以返回值。但return只能有一个,yield可以有多个;return是函数的结束,yield只是暂停,下一次调用会从上一次暂停的地方继续执行。为了展现“继续执行”,我们把上面的例子补充下:

def gen_fun():
    yield 1
    yield 2
    yield 3


g = gen_fun()
print(next(g))
print('-------上次执行-------')
for num in g:
    print(num)


# 输出:1
       -------上次执行-------
       2
       3

可以看到,for循环的值是从2开始的,因为第一个yield已经被next()执行了。有了生成器,函数的暂停和重启也就实现了。yield除了可以返回值也可以接收值(关于更多生成器的知识,单独总结

yield既可以实现生成器,也可以实现协程。为了使语义更明确,一般使用async-await实现协程。具体例子放在下面一起呈现。

(2)事件循环

可以使用while循环去遍历任务,监听,某任务有返回,则获取返回值做下一步操作(相对复杂)。也可以直接使用python(3.5以后)自带的异步IO——asyncio创建循环。且,其使用方式和多线程、多进程相似,保证了接口的一致性。

async def downloader(url):
    print('准备从{}下载内容'.format(url))
    # 模拟下载,不能使用time.sleep,会阻塞单线程
    await asyncio.sleep(3)
    return '这是从{}下载的内容,'.format(url)


async def handle(url):
    html = await downloader(url)
    print(html+'这是下载后进一步的处理')
    return html


if __name__ == "__main__":
    urls = ["http://www.baidu.com", "http://www.sina.com"]
    # 创建事件循环
    loop = asyncio.get_event_loop()
    start_time = time.time()
    tasks = [handle(url) for url in urls]
    loop.run_until_complete(asyncio.wait(tasks))
    print("耗时:", time.time()-start_time)


# 输出:准备从http://www.sina.com下载内容
       准备从http://www.baidu.com下载内容
       这是从http://www.sina.com下载的内容,这是下载后进一步的处理
       这是从http://www.baidu.com下载的内容,这是下载后进一步的处理
       耗时: 3.000380754470825

从最终输出打印的顺序和耗时就可以看出,协程是如何运作并实现并发的。当第一个url传到downloader的时候,我使用asyncio.sleep(3)模拟等待下载的时间。而线程并没有阻塞在这里,而是在等待的时候暂停了这个函数的执行(没有继续往下执行),并提交了第二个url请求。因此,先打印了两个“准备从xx下载内容”,当第一个url请求返回了结果,调度又切换回来继续执行。返回结果并交由handle处理。最后,从耗时也可以看出,两个请求都等待3s,使用协程的总耗时却不是6s。因此,我们使用协程实现了并发。

3.注意事项

协程里不能使用阻塞式代码(如time.sleep)和阻塞式第三方库(requests,pymysql)

关于asyncio更多知识,如其他创建任务方式,聚合任务,取消任务,获取任务返回等知识,单独总结

  • 作者:Jiangugu
  • 原文链接:https://blog.csdn.net/qq_45055172/article/details/121516948
    更新时间:2022-08-30 12:59:15