Python除了多线程和多进程,你还要会协程

Posted 攻城狮白玉

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python除了多线程和多进程,你还要会协程相关的知识,希望对你有一定的参考价值。

目录

前言

一、什么是阻塞

二、什么是协程

三、python编写协程的程序

四、使用asyncio报错

五、协程的优缺点

总结

写在后面


前言

我们一起学习了多线程和多进程,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除了多线程和多进程,你还要会协程的主要内容,如果未能解决你的问题,请参考以下文章

Python高阶(一) - 单线程、多线程和多进程的效率对比测试

从Python角度理解多线程和多进程

python线程进程和协程

Unity协程和线程的区别

python之线程进程和协程

python 多进程和多线程3 —— asyncio - 异步IO