目录
前言
一、什么是阻塞
二、什么是协程
三、python编写协程的程序
四、使用asyncio报错
五、协程的优缺点
总结
写在后面
我们一起学习了多线程和多进程,python里还有一个比较重要的概念,那就是协程。这里的协程并不是那个什么鬼携程出行。而是计算机里的一个概念。咱们在,通过学习多线程,我们知道,我们可以在一个进程里面开多个线程,来同时并行执行任务。但是,我们会发现,我们其实单个线程里面的CPU资源利用并没有达到最大。
有同学会问,为啥呢?咱们不是用了多线程了吗?是的,那之前咱们为啥要用多线程呢?因为我们在用requests库在进行请求的时候,线程是阻塞的,所以采用了多线程来“解决”爬取慢的问题,但是实际上每个线程里在执行的时候,还是有一段时间是阻塞的。此时对应线程里占用的CPU资源并没有进行工作的。
可能会被我绕晕了,不怕,下面咱们一一捋顺。
说明一下我的操作环境
前面我们提到阻塞,那什么是阻塞呢?这里咱们是指函数的阻塞调用是指调用结果返回之前,当前线程都会处于挂起状态(此时,CPU是不给线程分配时间片,线程处于暂停运行状态)。一般情况下,当程序处于IO操作(输入输出)时,线程都会处于阻塞状态。
input("请输入名字:")
resp = requests.get("https://blog.csdn.net/zhh763984017")
例如咱们在python里常用的input()函数,以及做HTTP请求时用到requests库的get请求,在网络请求返回数据之前,程序处于等待数据的阻塞状态。亦或者使用time.sleep()模块,让程序进行阻塞。
这些都是在没有接收完数据或者没有得到结果之前,是不会返回的。
import time def func(): print("攻城狮白玉") time.sleep(3)# 让当前线程处于阻塞状态 print("baiyu") if __name__ == '__main__': func()
先说一个生活中的通俗的例子,比如你要给女神她做饭吃(这是一个完整的程序,由多个函数组成)。
上面当你在做一件事情的时候,将等待时间用来做其他事情,这个动作就是协程。
协程,是一个微线程,可以看作是一种特殊函数的存在。协程就是当程序遇见了IO操作(阻塞调用)的时候,可以选择性的切换到其他任务上。
协程与线程的关系,就像线程与进程的关系一样。一个进程可以有多个线程,那一个线程也可以有多个协程。
想要通过python实现协程的功能,这里我们用到的是asyncio库
这里再强调一下,我用到的python版本是python3.7。
import asyncio
咱们需要在定义函数的时候,在函数定义前面加上 async 关键字,此时函数被声明为异步函数,当我们拿到调用这个函数的时候,会得到一个协程对象。
异步函数可以在函数的执行过程中,可以通过await关键字进行挂起。
如果await关键字后面跟的是同步操作的代码,那么程序没办法直接挂起,要等同步操作函数执行完成才挂起。
因此一般在异步操作的代码面前,要加上 await 关键字,来进行挂起。
说了这么多,贴一下代码解释一下~
import asyncio import time async def baiyu1(): print("攻城狮白玉") # time.sleep(3) # 当程序出现了同步操作时,异步操作就中断了 # 因此异步的休眠,我们用asyncio模块的sleep函数,这个是异步的 await asyncio.sleep(3) # 异步操作的代码 print("baiyu") async def baiyu2(): print("攻城狮白玉2") # time.sleep(3) await asyncio.sleep(3) # 异步操作的代码,通过await关键字挂起 print("baiyu2") async def baiyu3(): print("攻城狮白玉3") # time.sleep(3) await asyncio.sleep(3) print("baiyu3") if __name__ == '__main__': t1 = time.time() b1 = baiyu1() # 此时的函数的异步协程函数,此时函数执行得到的是一个协程对象。 b2 = baiyu2() b3 = baiyu3() tasks = [b1, b2, b3] # 一起启动多个任务(协程) asyncio.run(asyncio.wait(tasks)) t2 = time.time() print(f"程序执行完毕,耗时:{t2 - t1}")
当我们的异步函数体里面出现了同步操作,则会中断我们的异步操作,要等同步操作执行完。所以上面我把time.sleep()函数注释掉,换成asyncio.sleep()。同学们也可以自己尝试一下,看看执行的时间分别是多少。
下面是运行完上述代码的截图:
上面只是为了让大家理解一下怎么去写协程函数。下面介绍一下更好的协程调用方法。咱们可以写一个主协程函数,然后里面包含所有的协程函数,在里面进行注册和调用。
原来代码里的协程函数还是不用变,照写。只是多声明定义一个main协程函数,在程序启动时,一次性启动多个任务。代码如下:
async def main(): tasks = [ baiyu1(), baiyu2(), baiyu3() ] await asyncio.wait(tasks) if __name__ == '__main__': # 一次性启动多个任务(协程) asyncio.run(main())
这里声明的main函数要注意也是一个异步函数。
除了这个,还有要注意的一点就是,在python3.8版本,在注册异步函数时,要注意使用create_task()对于我们的异步函数进行包装。如下代码所示:
async def main(): #python3.8以后上面的这个tasks写法要更新 # 用asyncio.create_task包装起来 tasks = [ asyncio.create_task(baiyu1()), asyncio.create_task(baiyu2()), asyncio.create_task(baiyu3()) ] await asyncio.wait(tasks)
具体的完整代码如下:
import asyncio async def baiyu1(): print("攻城狮白玉") await asyncio.sleep(3) # 异步操作的代码 print("baiyu") async def baiyu2(): print("攻城狮白玉2") # time.sleep(3)#当程序出现了同步操作时,异步操作就终端了 await asyncio.sleep(3)#异步操作的代码 print("baiyu2") async def baiyu3(): print("攻城狮白玉3") time.sleep(3) print("baiyu3") async def main(): # python3.7使用tasks这个代码 tasks = [ baiyu1(), baiyu2(), baiyu3() ] #python3.8以后上面的这个tasks写法要更新 # 用asyncio.create_task包装起来 # tasks = [ # asyncio.create_task(baiyu1()), # asyncio.create_task(baiyu2()), # asyncio.create_task(baiyu3()) #] await asyncio.wait(tasks) if __name__=='__main__': # 一次性启动多个任务(协程) asyncio.run(main())
在爬虫领域的应用,多任务异步协程应用,在网络请求最耗时的东西给去掉。比如我们的requests的get请求,在下载图片。这样子在,整个爬虫就可以节省很多时间。接下来我会写一篇博客通过协程来爬取数据。
AttributeError: module 'asyncio' has no attribute 'run'
这个是因为使用的是python3.6的解析器,你把解析器换成时python3.7就好了
当await后面跟的是同步操作函数时,会报错
TypeError: object NoneType can't be used in 'await' expression
凡事有利就有弊,协程也是有它的优点跟缺点的。
优点:
缺点:
协程是在编程里面一个很重要的概念。协程是一个特殊函数,实现可以看作是将一系列的异步回调函数给组合串行运行,看起来是多任务并行的样子。由于是多任务的串行运行。所以协程本质上只是一个线程。所以就没有办法利用CPU的多核性能。
那协程要怎么样才能利用上CPU的多核性能呢?咱们可以采取协程+进程的方式来实现的。具体怎么实现,小伙伴们可以自己思考一下~
协程与线程的关系,就像线程与进程的关系一样。一个进程可以有多个线程,那一个线程也可以有多个协程。
如果觉得有用的话,麻烦一键三连支持一下攻城狮白玉,并把本文分享给更多的小伙伴。你的简单支持,我的无限创作动力