python与协程

写在前面

在执行一些IO密集型任务的时候, 程序往往会因为等待IO而阻塞. 比如在网络爬虫中, 如果我们使用requests函数库来进行请求的话, 如果网站的响应速度过慢, 程序会一直在等待网站的响应, 最后导致爬虫爬取的效率非常低.

协程可以用来进行加速, 它对于IO密集型仍无非常有效.

基本概念

在了解协程之前, 我们得了解阻塞, 非阻塞, 同步, 异步, 多进程, 协程等基本概念

1、阻塞

阻塞状态是指程序未得到所需计算资源时被挂起的状态. 程序在等待某个操作完成期间, 自身无法继续干别的事情, 则称该程序在该操作上是阻塞的.

常见的阻塞形式有: 网络I/O阻塞, 磁盘I/O阻塞, 用户输入阻塞等, 阻塞无处不在, 包括CPU切换上下文时, 所有的进程都无法真正干事情, 他们也会被阻塞, 如果是多核CPU, 则正在执行上下文切换操作的核不可被利用.

2、非阻塞

程序在等待某些操作过程中, 自身不被阻塞, 可以继续干别的事情, 则称该程序在该操作上是非阻塞的.

非阻塞并不是在任何程序级别、任何情况下都可以存在, 仅当程序封装的级别可以囊括独立子程序单元时, 它才可能存在非阻塞状态.

非阻塞的存在是因为阻塞的存在, 正是因为某个操作阻塞导致的耗时与效率地下, 我们才要把它变成非阻塞的.

3、 同步

不同程序单元为了完成某个任务, 在执行过程中需要靠某种通信方式以协调一致, 称这些程序单元是同步执行的.

简言之, 同步意味着有序.

4、异步

为了完成某个任务, 不同程序单元之间过程中无需通信协调, 也能完成任务的方式, 不相关的程序单元之间可以是异步的.

简言之, 异步意味着无序.

5、多进程

多进程就是利用CPU的多核优势, 在同一时间并行的执行多个任务, 可以大大提高执行效率.

6、协程

协程是一种用户态的轻量级线程.

协程拥有自己的寄存器上下文和栈. 协程调度切换时, 将寄存器上下文和栈保存到其他地方, 在切回来的时候, 恢复先前保存的寄存器上下文和栈, 直接操作栈则基本没有内核切换的开销, 可以不加锁的访问全局变量, 所以上下文切换非常快. 因此协程能保存上次调用时的状态, 即所有局部状态的一个特定组合, 每次过程重入时, 就相当于进入上一次调用的状态.

协程本质上是个单进程, 协程相对于多进程来说, 无须线程上下文切换的开销, 无需原子操作锁定及同步的开销, 编程模型也非常简单.

我们可以通过协程来实现异步操作, 不如网络爬虫的场景下, 我们发出一个请求之后, 需要等待一定的时间才能得到响应, 但其实在这个等待的过程中, 程序可以干许多其他的事情, 等到响应得到之后才切换回来继续处理, 这样可以充分利用CPU和其他资源, 这就是异步协程的优势.

协程的作用是在执行函数A时, 可以随时中断, 去执行另一个函数B, 然后中断继续执行函数A(自由切换). 但这一过程中并不是函数调用(没有调用语句).

关于同步异步阻塞非阻塞的通俗理解

老张爱喝茶,废话不说,煮开水。出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。

1 老张把水壶放到火上,立等水开。(同步阻塞)老张觉得自己有点傻

2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。

3 老张把响水壶放到火上,立等水开。(异步阻塞)老张觉得这样傻等意义不大

4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)老张觉得自己聪明了。所谓同步异步,只是对于水壶而言。普通水壶,同步;响水壶,异步。虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。所谓阻塞非阻塞,仅仅对于老张而言。立等的老张,阻塞;看电视的老张,非阻塞。情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import gevent
import time

# 并发下载示例
def task(pid, fac):
response = urllib2.urlopen('http://json-time.appspot.com/time.json')
result = response.read()
json_result = json.loads(result)
datetime = json_result['datetime']

print('Process %s %s' % (pid, datetime))
return json_result['datetime']

lists = range(0,10000)
fac = 'task name'

def synchronous(): # 同步一个线程执行函数
t = time.time()
for i in lists:
task(i, fac)
print(time.time() - t)

def asynchronous(): # 异步一个线程执行函数
t = time.time()
threads = [gevent.spawn(task, i, fac) for i in lists] # gevent.spawn 时,协程已经开始执行
gevent.joinall(threads) # gevent.joinall 只是用来等待所有协程执行完毕
print(time.time() - t)

# def asynchornoius2():
# t = time.time()
# x = Pool(40)
# for i in lists:
# x.imap(task)
# # x.join()
# print(time.time() - t)
#
#
# def asynchornoius3():
# t = time.time()
# x = Pool(40)
# for i in lists:
# x.imap_unordered(task, i, fac)
# x.join()
# print(time.time() - t)

print('synchronous:')
synchronous() # 同步执行时要等待执行完后再执行

print('asynchronous:')
asynchronous() # 异步时遇到等待则会切换执行

print('asynchronius:')
asynchornoius()

# print('-----------')
# asynchornoius2()
# print('-----------')
#
# asynchornoius3()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import gevent
from gevent.pool import Pool
from gevent import monkey; monkey.patch_all()

# 限制并发的 greenlets
pool = Pool(300)
def asynchornoius():
t = time.time()
greenlets = []
for i in lists:
greenlet = pool.spawn(task, i, fac)
greenlets.append(greenlet)
pool.join()
print(time.time() - t)
# 协程中避免使用全局变量来进行状态统计,或者结果收集。
# 若要收集结果,可以使用 value 属性来获取每个 greenlet 的返回值。
for greenlet in greenlets:
print greenlet.value

参考文献

Python中异步协程的使用方法介绍
gevent程序员指南
gevent Tricks
关于gevent的Timeout(超时)问题……
Gevent 性能和 gevent.loop 的运用和带来的思考
延时和锁
python中的协程深入理解
对Python协程之异步同步的区别详解
python3之协程
patch_all 不是一个好主意