异步编程(四)-----协程

什么是协程?百度上一大堆,随时可以查。我认为协程就是微线程,比线程还要小。为什么要引入协程?我们发现在线程使用中,有一个GIL锁,线程之间访问临界资源是互斥的,这都是不利于提升代码执行效率的。我们知道线程是CPU调度的最小单位,如果我们有一个线程,线程内包含多个协程,协程之间来回切换就设计不到CPU的切换,就会减小很多不必要的开销。协程和线程相比,切换是由代码的关键字完成的,代码自由度要高一些。协程的使用要比线程、进程麻烦一点。协程在处理IO繁忙型数据时,效率很高,一个线程可以并发上完个协程。说白了,在做异步爬虫时,效率很高。下边逐步开始探索。

目前python实现协程的方式大概有3中:yield关键字、asyncio模块、gevent模块。

yield这个词在操作系统中出现过,学过操作系统的同学对这个词应该不陌生。下边上代码:

 

def func1():
    print('111111')
    yield
    print('222222')
    yield
if __name__ == '__main__':
    gen1 = func1()
    gen1.__next__()
    gen1.__next__()

输出:

111111
222222

关键字yield的使用和迭代器就有关系了。在函数定义func1()有关键字yield,所以就不在是一个普通的函数定义。如果仍然用func1()调用执行函数,会发现没有任何输出。print(func1())之后,打印出来的是一个

<generator object func1 at 0x0000000003967248>

哦,generator 是生成器!如何执行这个生成器呢,就用.__next__()方法,或者用next(func1())也可以。

再来一个例子:

def func1():
    print('111111')
    yield
    print('222222')
    yield

def func2():
    print('333333')
    yield
    print('444444')
    yield
if __name__ == '__main__':
    gen1 = func1()
    gen2 = func2()
    gen1.__next__()
    gen2.__next__()
    gen1.__next__()
    gen2.__next__()

输出:

111111
333333
222222
444444

好像看起来通过yield关键字,可以实现两个函数之间的来回切换。确实,核心点在于,调用生成器方法.__next__() 会执行到函数yield部分,下次的.__next__() 会继续执行。这是比较简单的yield用法,但是能看出来确实起到协程切换的作用的了。用的不多。

gevent模块是很古老的python用于实现协程的模块,还在2.x的时代。在3.7之后,就出现asycio模块代替gevent模块了,所以gevent模块在这里就不在讨论。有新的,为什么还用旧的?早晚被淘汰。

asyncio是python3.7之后自带的,不用额外pip下载,直接在代码中import就可以。

下边着重说asyncio模块:

asyncio模块主要基于关键字async、await。

用async关键词修饰的函数叫做协程函数:

 

 这是最基本的协程函数定义,调用。

刚才的代码可以这样写:

import asyncio
async def fun():
    print("hello world!")

if __name__ == '__main__':
    reslut = fun()
    loop = asyncio.get_event_loop()
    loop.run_until_complete(reslut)

 

asyncio.run(reslut)

等同于:
loop = asyncio.get_event_loop()

loop.run_until_complete(reslut)

 

为了实现协程之间来回切换,必须创建一个“圈”,规定圈内的协程可以来回切换,这个“圈”就是asyncio.get_event_loop()创建的事件循环对象。好比是之前说的线程池,只有线程池内部的线程才可以切换。

loop.run_until_complete(reslut) 是将协程函数注册到事件循环中,也就是添加到线程池中,这样才能被执行。

 

再来一个例子,说明下await:

import asyncio
async def fun1():
    print('111111')
    await asyncio.sleep(1)
    print('222222')

async def fun2():
    print('333333')
    await asyncio.sleep(1)
    print('444444')

if __name__ == '__main__':
    reslut1 = fun1()
    reslut2 = fun2()
    loop = asyncio.get_event_loop()
    loop.run_until_complete(reslut1)
    loop.run_until_complete(reslut2)

输出:

111111
222222
333333
444444

从输出上看出来,好像没有完成并发操作,仍然是串行,那是因为代码写的是串行的代码。await 后边必须要跟能够等待的对象,asyncio.sleep()就是模拟IO等待的一个函数。如果写time.sleep()会报错。

await类似于前边的yield,和中断也很像,就是等,停下来执行await后边的函数,这么说和函数调用也差不多。。。

那问题来了,你不是要实现并发吗,怎么写就可以呢?

下边要是用task对象。

 

import asyncio
import time
async def fun1():
    print('111111')
    await asyncio.sleep(1)
    print('222222')

async def fun2():
    print('333333')
    await asyncio.sleep(1)
    print('444444')

async def main():
    reslut1 = fun1()
    reslut2 = fun2()
    task1 = asyncio.create_task(reslut1)
    task2 = asyncio.create_task(reslut2)
    await task1
    await task2
if __name__ == '__main__':
    start_time = time.time()
    asyncio.run(main())
    end_time = time.time()
    print(end_time-start_time)

输出:

111111
333333
222222
444444
1.0020570755004883

 

从输出和时间来看,两个函数fun1和fun2实现了并发执行。

 result1和result2是协程对象,自己可以print一下试试。asyncio.create_task(reslut1)、asyncio.create_task(reslut2) 是创建task对象,同时将任务添加到事件循环,这里其实是做了两件事!!!!所以在代码中并没有看到有关于“将任务添加到事件循环”。

 

再继续:

import asyncio
import time
async def fun1():
    print('111111')
    await asyncio.sleep(1)
    print('222222')
    return '1212'

async def fun2():
    print('333333')
    await asyncio.sleep(1)
    print('444444')
    return '3434'

async def main():
    print("main开始")
    reslut1 = fun1()
    reslut2 = fun2()
    task_list = [
        asyncio.create_task(reslut1),
        asyncio.create_task(reslut2)
    ]
    done,pending = await asyncio.wait(task_list)
    for ret in done:
        print('Task ret: ', ret.result())
    print("main结束")
    print(done)
if __name__ == '__main__':
    start_time = time.time()
    asyncio.run(main())
    end_time = time.time()
    print(end_time-start_time)

输出:

main开始
111111
333333
222222
444444
Task ret:  1212
Task ret:  3434
main结束
{<Task finished coro=<fun1() done, defined at C:/Users/W/PycharmProjects/test/class/协程.py:22> result='1212'>, <Task finished coro=<fun2() done, defined at C:/Users/W/PycharmProjects/test/class/协程.py:28> result='3434'>}
1.0020573139190674

done,pending = await asyncio.wait(task_list) 是关键代码,task_list是一个task对象的列表,asyncio.wait执行事件监听里的task对象。返回的done是一个set集合,集合中包含协程函数的返回值,也就是可以以这样的方式获取返回值,或者利用之前的回调机制,也可以。

 gather也可以实现wait功能,具体方法略有不同,请参考:

python—异步IO(asyncio)协程
posted @ 2021-01-20 18:23  理工—王栋轩  阅读(123)  评论(0编辑  收藏  举报