Python除了多线程和多进程,你还要会协程
Posted 攻城狮白玉
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python除了多线程和多进程,你还要会协程相关的知识,希望对你有一定的参考价值。
目录
前言
我们一起学习了多线程和多进程,python里还有一个比较重要的概念,那就是协程。这里的协程并不是那个什么鬼携程出行。而是计算机里的一个概念。咱们在,通过学习多线程,我们知道,我们可以在一个进程里面开多个线程,来同时并行执行任务。但是,我们会发现,我们其实单个线程里面的CPU资源利用并没有达到最大。
有同学会问,为啥呢?咱们不是用了多线程了吗?是的,那之前咱们为啥要用多线程呢?因为我们在用requests库在进行请求的时候,线程是阻塞的,所以采用了多线程来“解决”爬取慢的问题,但是实际上每个线程里在执行的时候,还是有一段时间是阻塞的。此时对应线程里占用的CPU资源并没有进行工作的。
可能会被我绕晕了,不怕,下面咱们一一捋顺。
说明一下我的操作环境
- win10
- python3.7
一、什么是阻塞
前面我们提到阻塞,那什么是阻塞呢?这里咱们是指函数的阻塞调用是指调用结果返回之前,当前线程都会处于挂起状态(此时,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操作(阻塞调用)的时候,可以选择性的切换到其他任务上。
- 微观上是一个任务一个任务的进行切换,串行执行线程的,切换条件一般是IO操作(函数阻塞)
- 宏观上,我们能看到的其实是多个任务一起在执行。尽管看起来是多任务一起执行,但实际上这都是在一个线程内。协程只是为了让CPU对我们这个线程一直分配资源,而从程序上实现的函数的调度的手段。
协程与线程的关系,就像线程与进程的关系一样。一个进程可以有多个线程,那一个线程也可以有多个协程。
三、python编写协程的程序
想要通过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请求,在下载图片。这样子在,整个爬虫就可以节省很多时间。接下来我会写一篇博客通过协程来爬取数据。
四、使用asyncio报错
AttributeError: module 'asyncio' has no attribute 'run'
这个是因为使用的是python3.6的解析器,你把解析器换成时python3.7就好了
当await后面跟的是同步操作函数时,会报错
TypeError: object NoneType can't be used in 'await' expression
五、协程的优缺点
凡事有利就有弊,协程也是有它的优点跟缺点的。
优点:
- 协程不是被操作系统内核所管理的,没有线程的启停消耗资源
- 一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
缺点:
- 协程的本质是个单线程,无法利用CPU多核的性能
- 由于是单线程,在进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
总结
协程是在编程里面一个很重要的概念。协程是一个特殊函数,实现可以看作是将一系列的异步回调函数给组合串行运行,看起来是多任务并行的样子。由于是多任务的串行运行。所以协程本质上只是一个线程。所以就没有办法利用CPU的多核性能。
那协程要怎么样才能利用上CPU的多核性能呢?咱们可以采取协程+进程的方式来实现的。具体怎么实现,小伙伴们可以自己思考一下~
协程与线程的关系,就像线程与进程的关系一样。一个进程可以有多个线程,那一个线程也可以有多个协程。
写在后面
如果觉得有用的话,麻烦一键三连支持一下攻城狮白玉,并把本文分享给更多的小伙伴。你的简单支持,我的无限创作动力
以上是关于Python除了多线程和多进程,你还要会协程的主要内容,如果未能解决你的问题,请参考以下文章