Python 协程的基本概念

Python 协程的基本概念

在学习 Python 基础的过程中,遇到了比较难理解的地方,那就是协程。刚开始看了廖雪峰老师的博客,没怎么看懂,后面自己多方位 google 了一下,再回来看,终于看出了点眉目,在此总结下。

什么是 yield 和 yield from

yield

在学习协程之前,要先搞懂几个基本语法,那就是 yield 和 yield from,这也是陆续困扰我几天的问题,等这两个概念弄懂以后,后面的事情就比较简单了。

  • yield 是一个关键字,当一个方法中带有 yield 时,它就不是一个普通的方法了,而是变成了一个所谓的“生成器”。
  • 生成器不会一下子把所有值都返回给你,可以使用 next() 方法来调用,来不断取值。
  • 当生成器中执行到 yield 的时候,会从 yield 处返回结果,并保留上下文,等下一次 next() 的时候,会从上次的 yield 处继续执行。
def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1
    return 'done'

f = fib(10)

print(next(f))
print(next(f))
print(next(f))
print(next(f))
print(next(f))
print(next(f))

上面是一个经典的斐波那契数列的生成函数,每次调用next都会从 yield 处返回结果。

输出:

1
1
2
3
5
Traceback (most recent call last):
  File "test.py", line 116, in <module>
    print(next(f))
StopIteration: done

遇到 return 会抛出异常,并将 return 的值包含在异常中抛出来。

一般我们不会一直调用 next() , 而是使用 for 循环:

for b in fib(5):
    print(b)

输出:

1
1
2
3
5

此处没有抛出异常,原因还待解释。。

使用 yield 一个一个地返回结果有什么作用?那是因为这样可以边循环边计算,边返回结果,不用创建完整的 list ,省去大量的内存空间。

接下来的关键点,可以通过 yield 传递参数!这个地方我也是弄了好久才弄明白的。。看下面的生产者和消费者的例子:

def customer():
    r = '404 empty'
    while True:
        print('star consume ..')
        n = yield r  # 2
        print('consuming {} ..'.format(n))
        r = '200 ok'


def producer(c):
    r = c.send(None)  # 1
    print(r)  # 3
    n = 0
    while n < 10:
        n += 1
        print('producing {} ..'.format(n))
        r = c.send(n)  # 4
        print('consumer {} return'.format(r))
    c.close()


if __name__ == '__main__':
    producer(customer())

输出:

star consume ..
404 empty
producing 1 ..
consuming 1 ..
star consume ..
consumer 200 ok return
producing 2 ..
consuming 2 ..
star consume ..
consumer 200 ok return
producing 3 ..
consuming 3 ..
star consume ..
consumer 200 ok return
producing 4 ..
consuming 4 ..
star consume ..
consumer 200 ok return
producing 5 ..
consuming 5 ..
star consume ..
consumer 200 ok return

先说明一下,send 方法可以给 yield 发送参数。

程序刚开始执行到“#1”处,这里必须先调用 send(None) 一下。此处可是有讲究的,学名叫“预激”,作用是先启动一下生成器,让它先卡在 yield ,所以此时程序在“#2”处中断了,并返回 r ,随后“#3”处打印出 “404 empty” 。

接下来程序来到“#4”处,又调用了 send 方法,此时 send 参数为1,所以“#2”处被重新激活,将 n 赋值为 1,然后继续向下执行。

接着又循环来到 “#2” 处,yield 将 r 返回,此处中断,来到“#4”处继续执行。如此不断循环,直到满足条件退出循环。

像这样,producer生产完,告诉customer消费,消费完再通知producer生产,这些事情都发生在同一个线程里面,因此没有多线程的锁和资源争夺的问题。至此,协程的初步面貌就已经浮出水面。

小 tip:
调用 send(None) ,就相当于调用 next()。

yield from

Python3.3 版本的 PEP 380 中添加了 yield from 语法,PEP380 的标题是 “syntax for delegating to subgenerator”(把指责委托给子生成器的句法)。由此我们可以知道,yield from 是可以实现嵌套生成器的使用。

yield from x 表达式对x对象做的第一件事是,调用 iter(x),获取迭代器。所以要求x是可迭代对象。

yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,使两者可以直接发送和产出值,还可以直接传入异常,而不用在中间的协程添加异常处理的代码。

这句话理解起来很麻烦,参考以下代码:

def A():
    yield from B()
    yield from C()

def B():
    yield '001'
    yield '002'
    yield '003'

def C():
    yield '004'
    yield '005'

if __name__ == '__main__':
    for s in A():
        print(s)

输出:

001
002
003
004
005

由此可见,生成器 A 通过 yield from 将任务下发给了生成器 B 和生成器 C 来执行了。yield from 可以很方便地拆分生成器,变为几个小生成器,方便代码管理。

什么是协程

根据维基百科给出的定义,“协程 是为非抢占式多任务产生子程序的计算机程序组件,协程允许不同入口点在不同位置暂停或开始执行程序”。

换句话说就是你可以中断函数执行,转而执行别的函数。听起来就像是你正在烧水,在此期间你可以做下一个事情,而不用等水烧开。

以前我们都是用多线程和锁来做这个事情,但是时常会担心线程安全和死锁问题,多线程切换还会产生额外的开销。现在使用协程,在一个线程里面就能完成这些任务,不用担心线程问题,没有使用任何锁,大大提高了执行效率。

上面生产者和消费者的例子,使用了 yield 简单的实现了一个协程代码。

asyncio

asyncio是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。

asyncio的编程模型就是一个消息循环。我们从asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO。

asyncio库使我们方便地实现协程,我们可以把多个协程方法扔到asyncio的消息循环中,asyncio就自动地帮我们调用并协调这些协程方法。

@asyncio.coroutine 装饰器,可以帮助我们把一个生成器装饰为协程方法。

import threading
import asyncio

@asyncio.coroutine
def hello(i):
    print('Hello world! {} {}'.format(i, threading.currentThread()))
    yield from asyncio.sleep(3)
    print('Hello again! {} {}'.format(i, threading.currentThread()))

loop = asyncio.get_event_loop()
tasks = [hello(1), hello(2)]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

输出

Hello world! 2 <_MainThread(MainThread, started 140736287097792)>
Hello world! 1 <_MainThread(MainThread, started 140736287097792)>
// 中间停3秒
Hello again! 2 <_MainThread(MainThread, started 140736287097792)>
Hello again! 1 <_MainThread(MainThread, started 140736287097792)>

可以看到在第一个任务执行时遇到 yield from ,这时候程序不会等着,而是马上开始下一个任务。当然先开始哪个任务是随机的。看打印出来的线程信息显示,两个任务是在同一个线程执行。

协程帮助我们在一个线程里面异步执行多个任务,里面的任务是并发进行的。

async/await

为了简化并更好地标识异步 IO,从Python 3.5 开始引入了新的语法async和await,可以让coroutine的代码更简洁易读。

使用也非常简单,只要把原来的 @asyncio.coroutine 替换为 async ,把 yield from 替换为 await 。

修改上一节的代码:

import threading
import asyncio

async def hello(i):
    print('Hello world! {} {}'.format(i, threading.currentThread()))
    await asyncio.sleep(3)
    print('Hello again! {} {}'.format(i, threading.currentThread()))

loop = asyncio.get_event_loop()
tasks = [hello(1), hello(2)]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

协程,有什么用?

asyncio可以实现单线程并发IO操作。如果仅用在客户端,发挥的威力不大。如果把asyncio用在服务器端,例如Web服务器,由于HTTP连接就是IO操作,因此可以用单线程+coroutine实现多用户的高并发支持。

aiohttp 应运而生,它是由 asyncio 实现的 HTTP 框架,帮助我们快速地搭建一个异步的 web 应用。

使用它要先安装:

pip install aiohttp

利用它我们用一小段代码搭建一个小应用:

import asyncio
from aiohttp import web


async def index(request):
    await asyncio.sleep(1)
    return web.Response(body=b'<h1>Index</h1>', content_type='text/html')


async def hello(request):
    await asyncio.sleep(1)
    text = '<h1>hello, {}!</h1>'.format(request.match_info['name'])
    return web.Response(body=text.encode('utf-8'), content_type='text/html')


async def init(loop):
    app = web.Application(loop=loop)
    app.router.add_route('GET', '/', index)
    app.router.add_route('GET', '/hello/{name}', hello)
    srv = await loop.create_server(app.make_handler(), '127.0.0.1', 8000)
    print('Server started at http://127.0.0.1:8000...')
    return srv


loop = asyncio.get_event_loop()
loop.run_until_complete(init(loop))
loop.run_forever()

总结

在这里,我们初步了解了下协程的基本概念。使用 yield 和 yield from ,并且利用 asyncio 库,可以组合成一个由协程组成的异步程序。async/await 帮助我们简化协程代码。利用协程可以写出很强大的 web 应用,进一步的深入我们后面再细细探究。

谁说代码不快乐,扫码关注,听猿哥叨叨,做一个快乐的程序猿。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容