Python协程从零开始到放弃

 2017-10-09  3,973
 

Author: lightless@Meili-inc

Date: 20171009

0x00 前言

很久以前就听说Python的async/await很厉害,但是直到现在都没有用过,一直都在用多线程模型来解决各种问题。最近看到隔壁的Go又很火,所以决定花时间研究下Python协程相关的内容,终于在翻阅了一裤衩的资料之后有了一些理解。

0x01 起:一切从生成器开始

以往在Python开发中,如果需要进行并发编程,通常是使用多线程/多进程模型实现的。由于GIL的存在,多线程对于计算密集型的任务并不十分友好,而对于IO密集型任务,可以在等待IO的时候进行线程调度,让出GIL,实现『假并发』。

当然对于IO密集型的任务另外一种选择就是协程,协程其实是运行在单个线程中的,避免了多线程模型中的线程上下文切换,减少了很大的开销。为了理解协程、async/await、asyncio,我们要从最古老的生成器开始。

回顾Python的历史,生成器这个概念第一次被提出的时候是在PEP 255中被提出的,当时的Python版本为Python2.2。我们都知道range()函数,现在考虑一下我们来编写一个自己的range()函数,最直接最容易想到的方法也许是这样:

def my_range(max_number):
sequence = []
index = 0
while index < max_number:
sequence.append(index)
index += 1
return sequence

当你想创建一个很小的序列的时候,例如创建从0到100这样的列表,似乎没什么问题。但是如果想创建一个从0到999999999这么大的列表的话,就必须要创建一个完整的,长度是999999999的列表,这个行为非常占用内存。于是就有了生成器,用生成器来改写这个函数的话,会是下面这个样子:

def lazy_range(max_number):
index = 0
while index < max_number:
yield index
index += 1

当函数执行遇到yield的时候,会暂停执行。这样只需在内存中维护可以存储一个整数的内存空间就可以了。如果对生成器/迭代器不理解的话,可以参考Stack Overflow上的这篇高票回答:传送门

0x02 承:协程诞生

到这里可能还和协程没什么关系,但是实际上这已经是Python协程的雏形了,我们来看看维基上对于协程的定义:

Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing multiple entry points for suspending and resuming execution at certain locations.

从某些角度来理解,协程其实就是一个可以暂停执行的函数,并且可以恢复继续执行。那么yield已经可以暂停执行了,如果在暂停后有办法把一些value发回到暂停执行的函数中,那么Python就有了『协程』。于是在PEP 342中,添加了“把东西发回已经暂停的生成器中”的方法,这个方法就是send(),并且在Python2.5中得到了实现。利用这个特性我们继续改写range()函数:

def smart_range(max_number):
index = 0
while index < max_number:
jump = yield index
if jump is None:
jump = 1
index += jump

就这样,整个生成器的部分似乎已经进入了stable的状态,但是在Python3.3中,这个情况发生了改变。在PEP 380中,为Python3.3添加了yield from,这个东西可以让你从迭代器中返回任何值(这里用的是迭代器,因为生成器也是一种迭代器),也可以让你重构生成器,我们来看这个例子:

def lazy_range(max_number):
index = 0 def gratuitous_refactor():
while index < max_number:
yield index
index += 1
yield from gratuitous_refactor()

这个特性也可以让生成器进行串联,使数据在多个生成器中进行传递。历史发展到这里,协程的出现似乎已经就差一步了,或者这里说是异步编程更恰当。在Python3.4中加入了asyncio库,使Python获得了事件循环的特性(关于事件循环的内容这里不再赘述)。asyncio + 生成器已经达到了异步编程的条件,在Python3.4中,我们就可以这样实现一个异步的模型:

import asyncio

@asyncio.coroutine
def counttdown(number, n):
while n > 0:
print("T-minus", n, "({})".format(number))
yield from asyncio.sleep(1)
n -= 1 loop = asyncio.get_event_loop()
tasks = [
asyncio.ensure_future(counttdown("A", 2)),
asyncio.ensure_future(counttdown("B", 5)),
]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

这里的asyncio.coroutine装饰器是用来标记这个函数是一个协程的,因为asyncio要求所有要用作协程的生成器必须由asyncio.coroutine装饰。在这段代码中,事件循环会启动两个countdown()协程,他们会一直执行,直到遇到了yield from asyncio.sleep(),会暂停执行,并且将一个asyncio.Future对象返回给事件循环。事件循环会监控这个asyncio.Future对象,一旦其执行完成后,将会把这个Future的执行结果返回给刚刚因为这个Future暂停的协程,并且继续执行原协程。

从这个具体的例子出发,抽象点来看,实际上这个过程变成了:

  1. 你可以对任何asyncio.Future的对象进行yield from,将这个Future对象交给事件循环;
  2. 暂停执行的协程将等待这个Future的完成;
  3. 一旦Future获取到事件循环,并执行完所有的代码;
  4. 事件循环感知到Future执行完毕,原暂停的协程会通过send()方法获取Future对象的返回值并且继续执行;

0x03 转:从yield from到await

终于到了最激动人心的地方,在Python3.5中,添加了types.coroutine装饰器以及async defawait。我们先来看一下Python3.4和Python3.5中如何定义一个协程函数:

# python34
@asyncio.coroutine
def py34_function():
yield from work() # python35
async def py35_function():
await work()

看起来Python3.5中定义协程更为简单了,但是实际上生成器和协程之间的差别变的更加明显了。这里先要指出两个个注意点:

  1. await只能用于async def的函数中;
  2. yield from 不能用于async def的函数中;

除此之外,yield fromawait可以接受的对象是略有区别的,await接受的对象必须是一个awaitable对象。什么是awaitable对象呢,就是一个实现了__await()__方法的对象,而且这个方法必须返回一个不是协程的迭代器。满足这两个条件,才算是一个awaitable对象,当然协程本身也是awaitable对象,因为collections.abc.Coroutine继承了collections.abc.Awaitable。换句话说,await后面可接受的对象有两种,分别是:协程awaitable对象,当然协程也是awaitable对象。

在Python3.6中,这种特性继续被发扬光大,现在可以在同一个函数体内使用yieldawait,而且除此之外,也可以在列表推导等地方使用async forawait语法。

result = [i async for i in aiter() if i % 2]
result = [await func() for fun in funcs if await condition()] async def test(x, y):
for i in range(y):
yield i
await asyncio.sleep(x)

0x04 合:尾声

到这里整个协程的历史已经是回顾完了,对于Python中的协程也有了一些理解,但是如何在实际中使用协程可能还有一些疑惑以及理解不够深刻的地方,准备继续研究几天,在下一篇文章讲一下实际场景中协程的运用和项目中遇到的问题。最后附上一些不错的文献:

How the heck does async/await work in Python 3.5?

Python协程:从yield/send到async/await

Python黑魔法 --- 异步IO( asyncio) 协程

Keynote at PyCon Brasil 2015 (Screencast)

 
转自 https://www.secpulse.com/archives/61398.html

[转载] Python协程从零开始到放弃的更多相关文章

  1. day-5 python协程与I/O编程深入浅出

    基于python编程语言环境,重新学习了一遍操作系统IO编程基本知识,同时也学习了什么是协程,通过实际编程,了解进程+协程的优势. 一.python协程编程实现 1.  什么是协程(以下内容来自维基百 ...

  2. Python协程与Go协程的区别二

    写在前面 世界是复杂的,每一种思想都是为了解决某些现实问题而简化成的模型,想解决就得先面对,面对就需要选择角度,角度决定了模型的质量, 喜欢此UP主汤质看本质的哲学科普,其中简洁又不失细节的介绍了人类 ...

  3. python 协程与go协程的区别

    进程.线程和协程 进程的定义: 进程,是计算机中已运行程序的实体.程序本身只是指令.数据及其组织形式的描述,进程才是程序的真正运行实例. 线程的定义: 操作系统能够进行运算调度的最小单位.它被包含在进 ...

  4. 5分钟完全掌握Python协程

    本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理 1. 协程相关的概念 1.1 进程和线程 进程(Process)是应用程序启动的实例,拥有代码.数据 ...

  5. Python 协程总结

    Python 协程总结 理解 协程,又称为微线程,看上去像是子程序,但是它和子程序又不太一样,它在执行的过程中,可以在中断当前的子程序后去执行别的子程序,再返回来执行之前的子程序,但是它的相关信息还是 ...

  6. 终结python协程----从yield到actor模型的实现

    把应用程序的代码分为多个代码块,正常情况代码自上而下顺序执行.如果代码块A运行过程中,能够切换执行代码块B,又能够从代码块B再切换回去继续执行代码块A,这就实现了协程 我们知道线程的调度(线程上下文切 ...

  7. 从yield 到yield from再到python协程

    yield 关键字 def fib(): a, b = 0, 1 while 1: yield b a, b = b, a+b yield 是在:PEP 255 -- Simple Generator ...

  8. 关于python协程中aiorwlock 使用问题

    最近工作中多个项目都开始用asyncio aiohttp aiomysql aioredis ,其实也是更好的用python的协程,但是使用的过程中也是遇到了很多问题,最近遇到的就是 关于aiorwl ...

  9. 用yield实现python协程

    刚刚介绍了pythonyield关键字,趁热打铁,现在来了解一下yield实现协程. 引用官方的说法: 与线程相比,协程更轻量.一个python线程大概占用8M内存,而一个协程只占用1KB不到内存.协 ...

随机推荐

  1. 【转】 面向对象(OO)程序设计

    前言 本文主要介绍面向对象(OO)程序设计,以维基百科的解释: 面向对象程序设计(英语:Object-oriented programming,缩写:OOP),指一种程序设计范型,同时也是一种程序开发 ...

  2. 【C#】权限修饰符

    这个看了蛮多遍的,但是由于有一些一直不用,老是忘记,记录一下:) private  成员只能由同一个类(class)类型中的其他成员访问. family  成员可由派生类访问,不管那些类型是否在用一个 ...

  3. [javaSE] 网络编程(TCP-并发上传图片)

    客户端: 1.服务端点 2.读取客户端已有的图片数据 3.通过socket输出流将数据发给服务端 4.读取服务端反馈信息 5.关闭 获取Socket对象,new出来,构造参数:String的服务端ip ...

  4. 02-Http请求与响应全解

    什么是协议 约束双方规范的一个准则 什么是HTTP协议 HTTP,超文本传输协议(HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议 所有的WWW文件都必须遵 ...

  5. Oracle数据库基本操作(二) —— 视图、序列、索引、同义词

    一.视图(Views)与 同义词 1.视图:实际上是对查询结果集的封装,视图本身不存储任何数据,所有的数据都存放在原来的表中; 在逻辑上可以把视图看作是一张表 2.作用: 封装查询语句,简化复杂的查询 ...

  6. Thymeleaf学习记录(4)--$/*/#/@语法

    表达式符号 Thymeleaf对于变量的操作主要有$\*\#三种方式: 变量表达式: ${...},是获取容器上下文变量的值. 选择变量表达式: *{...},获取指定的对象中的变量值.如果是单独的对 ...

  7. ES6框架的搭建

    1.引入traceur.js  http://google.github.io/traceur-compiler/bin/traceur.js 2.将Traceur编译器用于网页 new traceu ...

  8. MySQL数据库(7)----数据库的选择、创建、删除和更改

    1.选择数据库 使用 USE 语句可以选择数据库,并把它指定为MySQL服务器连接的默认(当前)数据库: USE db_name; 要想选择数据库,用户必须要具备相应的访问权限:否则,会出现错误提示. ...

  9. Function Object in C++

    Function object is very userful to use member function or non-member function as callback mechanism, ...

  10. Sql Server 2012 Local DB发布到服务器端后无法访问

    背景 基于Windows认证的Web application, 通过Visual Studio 2013创建的LocalDB位于App_Data目录下 现象 本地调试没有任何问题.发布到服务器(Win ...