进程是资源分配的最小单位,而线程则是cpu调度的最小单位。所以cpu最小只能调度一个线程,而对于一个线程内部的协程,cpu是无法进行切换的,即使遇到I/O阻塞。它只能通过程序内部代码自己来调度。
比如有10个任务,要求要并发的执行,我们有如下方法可以做到:
1. 开启多个进程并发执行,操作系统切换+保持状态
2.开启多线程并发执行,操作系统切换+保持状态
3.开启协程并发执行,程序自己控制cpu在三个任务间切换+保持状态。
使用协程的方式有一个缺点,假如三个任务同时遇到了阻塞,那么操作系统可能将cpu切换走。
但优点也很明显:
开销小,执行速度快,同时能一直占用cpu资源(操作系统认为一直在执行),当遇到I/O操作时能自动切换到其它协程。
从上面可以看出,并发的本质 就是 :在保存状态的情况下切换。
greenlet模块只能完成切换,但遇到I/O时无法切换
from greenlet import greenlet
def eat(name):
print('%s eat 1' % name)
g2.switch('andy')
print('%s eat 2' % name)
g2.switch()
def play(name):
print('%s play 1' % name)
g1.switch()
print('%s play 2' % name)
g1.greenlet(eat)
g2.greenlet(play)
g1.switch('andy')
#输出
andy eat 1
andy play 1
andy eat 2
andy play 2
它的执行过程如下:
主线程的 g1.switch('andy'), 然后执行eat函数, 打印 andy eat 1, 接着切换到g2, 打印 andy play 1, 现次遇到切换 andy eat 2, 最后再次切换g2,
打印andy play 2.
但我们的目的不仅仅是切换,而是在保存状态的切换的状态下,而且在遇到IO的情况下也能自动切换。
下面看它的改进版:
import gevent
def eat(name):
print('%s eat 1' % name)
gevent.sleep(2)
print('%s eat 2' % name)
def play(name):
print('%s play 1' % name)
gevent.sleep(1)
print('%s play 2' % name)
g1 = gevent.spawn(eat, 'andy')
g2 = gevent.spawn(play, 'jack')
g1.join()
g2.join()
#或者gevent.joinall([g1,g2])
print('主')
#输出:
andy eat 1
jack play 1
jack play 2
andy eat 2
主
这里有一点需要注意,假如我不写Join的话,那么上面的两个函数是不会执行的,这里只有一个主线程,主线程正常情况下会等待所有蜚守护线程的子线程执行完再结束 ,便这里没有其它子线程,主线程完了,也就整个执行完了,内部协程还没来得及执行(这里的理解 可能不正确)。joIn也是ducktype。
另外 这里使用的是gevent.sleep, 这是gevent能识别的阻塞,而如果换成time.sleep是无法识别的,也就无法切换。
所以就需要打补丁,即monkey.patch_all() 它的作用相当于对前代码下面的所有任务中的阻塞打上标记,遇到这种标记的的IO时就会切换。
下面是最终版本的代码
import time
import gevent
from gevent import monkey
monkey.patch_all()
def eat(name):
print('%s eat 1' % name)
time.sleep(2)
print('%s eat 2' % name)
def play(name):
print('%s play 1' % name)
time.sleep(1)
print('%s play 2' % name)
g1 = gevent.spawn(eat, 'andy')
g2 = gevent.spawn(play, 'jack')
# g1.join()
# g2.join()
gevent.joinall([g1,g2])
print('主')
#输出
andy eat 1
jack play 1
jack play 2
andy eat 2
主
注意:from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前
一般在工作中我们都是进程+线程+协程的方式来实现并发,以达到最好的并发效果,如果是4核的cpu,一般起5个进程,每个进程中20个线程(5倍cpu数量),每个线程可以起500个协程,大规模爬取页面的时候,等待网络延迟的时间的时候,我们就可以用协程去实现并发。 并发数量 = 5 * 20 * 500 = 50000个并发,这是一般一个4cpu的机器最大的并发数。nginx在负载均衡的时候最大承载量就是5w个 。