Python3学习笔记:清晰理解协程

2019-03-27  本文已影响0人  码途有道

线程和进程

在了解协程之前,我们先简单了解一下进程与线程,并发与并行的概念。

从上述介绍中,可以看出,一个进程中是可以同时执行多个线程的(就像浏览器中的播放视频和下载文件可以同时进行一样)。并且,一个进程中至少会有一个线程。

什么是协程

什么是协程?我们先来看下对协程的概括:协程被称为微线程或者纤程,是一种用户态的轻量级线程。
其本质就是一个单线程,协程的作用就是在一个线程中人为控制代码块的执行顺序。(记住这句就可以了)

具体解释如下:
在一个线程中有很多函数,我们称这些函数为子程序。当一个子程序A在执行过程中可以中断执行,切换到子程序B,执行子程序B。而在适当的时候子程序B还可以切换回子程序A,去接着子程序A之前中断的地方(即回到子程序A切换到子程序B之前的状态)继续往下执行,这个过程,我们可以称之为协程。看到这段解释,是不是觉得和yield关键字的操作很相似。

Tip:关于yield和生成器不了解的,可以看[《Python3学习笔记:清晰理解迭代器、生成器以及yield表达式》][Python3学习笔记:清晰理解迭代器、生成器以及yield表达式],下面会涉及到。

协程的工作方式

为了方便理解,我们用一个简单示例来展示协程的工作大致工作方式,以下是一个经典的 生产者-消费者 模型:

import time

def consumer():
    print("[消费者]:我准备好接收来自生产者的消息了!")
    while True:
        y = (yield)
        time.sleep(1)
        print("[消费者]:接收到来自生产者的消息  %s" % y)


def producer():
    c = consumer()
    c.send(None)
    i = 1
    while i < 5:
        time.sleep(1)
        print("[生产者]:向消费者发送消息 %s" % i)
        c.send(i)
        i += 1
    c.close()

producer()

打印结果:

[消费者]:我准备好接收来自生产者的消息了!
[生产者]:向消费者发送消息 1
[消费者]:接收到来自生产者的消息  1
[生产者]:向消费者发送消息 2
[消费者]:接收到来自生产者的消息  2
[生产者]:向消费者发送消息 3
[消费者]:接收到来自生产者的消息  3
[生产者]:向消费者发送消息 4
[消费者]:接收到来自生产者的消息  4

示例中,消费者是个生成器,生产者与消费者的交互如下:

  1. 生产者先在内部通过c.send(None)激活了消费者,生产者中断执行,消费者获取程序控制权开始执行
  2. 消费者执行中遇到yield后被挂起,停止了执行,等待生产者发送消息,并将程序控制权返回给生产者,生产者接着上次中断的地方继续执行
  3. 生产者进入while循环,每隔1秒,向消费者发送一条消息,消费者的yield收到消息后,从上次yield中断的地方继续往后执行,当再次遇到yield后重复【2】中的流程

可以看到,消费者每次都需要等待生产者传入消息之后才会继续执行之后的任务。

以上是协程的一个大概的工作方式,可以人为控制子程序(即函数)的执行顺序,人为的切换子程序的执行。上述示例比较简单,也并不是真正的协程,这是协程的工作方式而已。下面我们来看一下真正的协程是什么样的。

真正的协程实现

我们下面要实现的真正的协程,需要使用python标准库中的asyncio模块。asyncio是在python3.4时引入的,在python3.5的时候,asyncio添加了两个关键字aysncawaitasync关键字可以将一个函数修饰为协程对象,await关键字可以将需要耗时的操作挂起,一般多是IO操作。(注:如果使用的是python3.4请用@asyncio.coroutine代替asyncyield from代替await

import asyncio
import time


start = time.time()


async def teach_student(n, t):
    print("开始辅导学生 %d  。。。" % n)
    await asyncio.sleep(t)
    print("学生 %d  结束辅导" % n)


t1 = teach_student(1, 2)
t2 = teach_student(2, 1)
t3 = teach_student(3, 3)

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(t1, t2, t3))
loop.close()
end = time.time()
print("执行总时间:", end-start)

打印结果:

开始辅导学生 1  。。。
开始辅导学生 2  。。。
开始辅导学生 3  。。。
学生 2  结束辅导
学生 1  结束辅导
学生 3  结束辅导
执行总时间: 2.996910333633423

上面是模拟一段1位老师3位辅导学生功课的场景,就是我们在一开始提到的并发中的例子。下面我们来解析一下上面的示例:

以上就是一个真正的协程示例,如果老师辅导学生,每次都等辅导的学生完成功课后,在辅导下一个学生,会花费足足7秒时间(同步顺序执行)。而协程的方式,则是老师在辅导完一位学生后,就让学生自己做功课,老师不等待,立马开始辅导下一位学生,极大的缩短了时间,提高了效率(异步执行)。并且从以上示例也可以看出使用asyncio实现的协程的一些特性:

为什么要使用协程

在了解为什么要使用协程之前,我们要先了解多线程在单核CPU下的执行情况。当你的电脑只是单核CPU时,所谓的多线程的执行,其实只是CPU在不停的切换执行的线程,例如线程A执行0.0.1秒后,切换到线程B执行0.01秒,再切换到线程C执行0.01秒,但是同一时间,只有一个线程在执行。只有在多核CPU时,多线程才有可能做到几个线程同时执行。

但是,我们在Python中这一情况又有所不同。我们广泛使用的Python解释器是CPython(我们经常说的Python2和Python3 就是解释器CPython的Python版本) ,除此之外还有JPython、pypy等其他解释器。但是CPython才是使用最广泛的。而CPython解释器中存在GIL锁(全局解释器锁 Global Interpreter Lock,注意GIL不是Python语言的特性,只存在CPython解释器中),它的作用就是防止多线程时线程抢占资源,所以在同一时间只允许一个线程在执行,即使在多核CPU情况下也是一样 ,所以CPU的单核和多核对于多线程的运行效率并没有多大帮助,还是线程之间的不停切换。

基于以上情况,在一些多线程的场景时,我们就可以使用协程来代替多线程,并且可以做的更灵活。我们下面来看下协程的优势:

所以,在处理一些高并发场景时,有时协程比多线程更加适合,比如做爬虫时。

上一篇下一篇

猜你喜欢

热点阅读