一文带你了解Python中的协程

一文带你了解Python中的协程

大家好!我是栗子鑫,今天分享一道当时面试“菊厂”的一道面试题:了解协程吗?(当时笔者第一次听到这个词,面试时尴尬地回答了没了解过!)面试完,立马去学习相关知识,正文如下,希望对你们有所帮助。



01


相关概念



在介绍协程前先介绍几个概念

  1. 进程

    进程就是一个程序在一个数据集上的一次动态执行过程,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。

  2. 线程

    线程也叫轻量级进程,它是一个基本的CPU执行单元,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

  3. 协程

    协程是在一个线程执行过程中可以在一个子程序的预定或者随机位置中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。他本身是一种特殊的子程序或者称作函数

  4. GIL

    GIL: 全局解释器锁(英语:Global Interpreter Lock,缩写GIL),是计算机程序设计语言解释器用于同步线程的一种机制,它使得任何时刻仅有一个线程在执行。即便在多核心处理器上,使用 GIL 的解释器也只允许同一时间执行一个线程。

进程、线程和协程三者的比较

  1. 协程既不是进程,也不是线程,协程仅仅是一个特殊的函数,协程跟他们就不是一个level。

  2. 一个进程可以包含多个线程,一个线程可以包含多个协程,多个协程只能串行化运行。

    如何理解协程只能串行化运行

    协程虽然是一个特殊的函数,但仍然是一个函数。一个线程内可以运行多个函数,但是这些函数都是串行运行的。当一个协程运行时,其他协程必须挂起。如果有多核CPU的话,多个进程或一个进程内的多个线程是可以并行运行的,但是一个线程内的多个协程却绝对串行的,无论有多少个CPU(核)

  3. 协程与进程一样,它们的切换都存在上下文切换问题。

    上下文切换对比 进程 线程 协程
    切换时机 操作系统 操作系统 用户
    切换内容 根据操作系统自习的切换策率自己不敏感 根据操作系统自习的切换策率自己不敏感 用户自己决定
    切换内容的保存 页全局目录、内核栈、硬件上下文 内核栈、硬件上下文 硬件上下文
    切换过程 保存于内核栈中 保存于内核栈中 保存于用户自己的变量(用户栈或者堆)



02


协程的作用(优点)



由于python的GIL的存在,导致Python多线程性受到各种限制能甚至比单线程更糟。这个时候协程就发挥作用,在线程执行任务时,受制于GIL,不能并行,反而因为上下文切换更耗时,这个时候协程正可以弥补发挥作用。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。同时不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。



03


协程的使用场景



相比进程和线程,协程应该是最轻量的一种并发方案,任务切换完全由程序员自由控制,只有当上一个任务交出控制权下一个任务才能开始执行,协程和线程更适合处理 I/O 密集的场景,特别是 Python 中的多线程实际上也只是单线程中执行;而对于 CPU 密集的场景来说,多进程、多机器、多处理器才能提高程序的运行速度。

IO密集 vs CPU密集

IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务。

CPU密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。

两者主要区别:

  • IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少。

  • CPU密集型主要消耗CPU资源



04


协程的实现方式



Python对协程的支持是通过生成器(generator)实现的。具体是什么生成器请移步到上一篇博客  python 三大器

举个🌰

def A():
  r = ''
  while True:
    print('A 挂起')
      n = yield r
      if not n:
        return
      print('A 恢复---并接收到数据 %s...' % n)
      r = '200 OK'


def B(c):
  print("准备启动协程")
  c.send(None)
  n = 0
  while n < 5:
    n = n + 1
    print('B 发送数据 %s...' % n)
    r = c.send(n)
    print('B 处理结果: %s' % r)
  c.close()
c = A()
B(c)

代码解释:

A函数是一个生成器,把A传入到B以后:

  1. 首先执行A.send()启动生成器(其功能类似于next(c));
  2. 然后,一旦生产了东西,通过c.send(n)切换到A执行;
  3. A通过yield拿到消息,处理,又通过yield把结果传回;
  4. B拿到A处理的结果,继续生产下一条消息;
  5. B决定不生产了,通过c.close()关闭A,整个过程结束;

整个流程无锁,由一个线程执行,AB协作完成任务,所以称为“协程”,而非线程的抢占式多任务。可以把A比作消费者,B比作生产者。传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高。

运行结果:

一文带你了解Python中的协程
运行结果

上述实现方式是通过yield方式实现的,在python3.7后使用async def + await的方式定义协程,

async 用来声明一个函数为异步函数,异步函数的特点是能在函数执行过程中挂起,去执行其他异步函数,等到挂起条件消失后,再回来执行。await 用来用来声明程序挂起,比如异步程序执行到某一步时需要等待的时间很长,就将此挂起,去执行其他的异步程序。await 后面只能跟异步程序或有__await__属性的对象,因为异步程序与一般程序不同。

具体🌰如下:

async def func1(index):
    r = await func2(index)
    print(index,r)


async def func2(index):
    print(index)
    await asyncio.sleep(5#这里模仿异步I/O
    return index

if __name__ == '__main__':
    index_list = [1,2,3]
    loop = asyncio.get_event_loop()
    task = [asyncio.ensure_future(func1(i)) for i in index_list]
    loop.run_until_complete(asyncio.wait(task))
    loop.close()
#运行结果
1
2
3
1 1
2 2
3 3
# 可以看出 首先将所有的 index打印出来 应为每一个index 对饮的r 都有个I/O为5秒的等待 协程让这些I/O挂起了先执行了 所有的打印index操作 



05


总结



本文分享关于python协程的概念,同时也介绍了基本的相关概念,如进程、线程、GIL等。希望对你有帮助,欢迎交流。简要总结协程只是一个特殊的函数,也不是并行,只是任务交替执行任务,在存在阻塞I/O情况,能够异步执行,提高效率。最后祝大家早日拿到心意的offer

关注六只栗子,面试不迷路!


作者    栗子鑫

编辑   一口栗子  

原文始发于微信公众号(六只栗子):一文带你了解Python中的协程

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/88624.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!