说到并发编程,大家容易想到的就是:进程、线程、协程、异步IO。四者在实现上却有共通之处,不外乎调度二字。
进程:操作系统进程系统调度,调度号:pid,基本由操作系统提供调度支持
线程:操作系统线程调度,调度号:TCB,虚拟机提供一部分支持
协程:程序自己进行调度,调度号:函数名,全部由程序自身完成。
异步IO:由消息中间件负责调度,调度号:消息队列。
进程、线程、协程它们三个实现的是时间复用,达到逻辑上的同步;而异步IO则仅仅实现了应用的解耦合,必须要配合前面三者中的一个或多个来实现并发的处理;
下面专门讲协程
协程,顾名思义就是协同合作的程序,一个程序里可以有很多协程,这些协程在主线程的调度下,表现得像是并发执行得一样;这样得好处就是:轻量级。
人们常说:线程是轻量级的进程;到了协程这里,我们可以说协程是最轻量级的执行单元;不会再有比协程更轻的了;一个协程可以是一个函数或者任何可调用对象,它的调度也完全由程序自身控制,因此可以将其调度开销按照自身需要降到最低。这就是它的优势所在;
本文刚开始就说到了,处理并发的核心思想就是:调度;所以不要觉得协程是多么新的东西;早在你用C语言编写俄罗斯方块程序的时候,就用过了;俄罗斯方块需要一个专门统计和显示分数的函数,还需要一个控制方块运动翻转的函数,这两个函数需要是并发执行的;你当时的做法肯定不是用多线程,而是像下面一样:
while(1){
sleep(0.05)
counter();
move();
}
现在你知道协程是什么东西了吧,就是上面的counter() move()
下面由浅入深的讲python协程的3个境界:“自己实现的协程”、“”、“利用第三方库实现的协程”
说到并发,最简单的模型就是生产者-消费者模型:生产者每隔一段时间生产一个产品,而消费者也每隔一定时间消费一个产品,生产者一般认为只有一个,消费者可以有多个;
自己实现的协程
直接上代码,有点类似俄罗斯方块的实现
from time import sleep,time
from Queue import Queue
queue=Queue(10)
class producer():
def __init__(self, interval):
self.interval = interval
self.count = 1
self.state = "free"
def run(self):
if self.state == "free":
if not queue.full():
self.start_time = time()
print "producer is making cake[%d]"%(self.count)
self.state = "busy"
else:
print "more than 10 cakes exits!,producer is waiting"
if self.state == "busy":
if time()-self.start_time > self.interval:
queue.put("cake[%s]"%self.count)
self.count+=1
self.state = "free"
class consumer():
def __init__(self,name, interval, producer):
self.interval=interval
self.producer=producer
self.state="free"
self.cake=None
self.name=name
def run(self):
if self.state=="free":
if not queue.empty():
self.cake=queue.get()
self.start_time = time()
self.state="busy"
else:
print "no cake exits, %s is waiting!"%self.name
if self.state=="busy":
if time() - self.start_time > self.interval:
print "%s finished eating %s"%(self.name, self.cake)
self.state="free"
def main():
p = producer(1)
c1 = consumer("mom",2, p)
c2 = consumer("child",3, p)
while(True):
sleep(0.1)
p.run()
c1.run()
c2.run()
main()
利用语言特性yield实现的协程
还是那句话,要实现并发就是实现调度;而调度的本质是什么?是交警叔叔一个手势让一辆车停,让令一辆车跑;我们这里的车是函数;python里面如果没有使用yield的函数,我们是无法让他执行到一半停止的,所以我们才会像上面那样在while循环里每次调用一次这个函数,而又通过类对象存储状态,使得看上去好像有一个东西跑跑听听一样;有了yield这个语言特性,我们就可以将状态直接当作函数的状态了;这使得我们要做的工作少了很多,比如上面的sleep(0.1)
代码如下:
from Queue import Queue
from time import time
queue=Queue(10)
def producer(name, interval):
num=0
state="free"
while True:
if state=="free":
if not queue.full():
state="busy"
start_time=time()
print "%s is making cake[%d]"%(name, num)
num+=1
else:
print "more than 10 cakes exists, %s is waiting"%name
yield
continue
if state=="busy":
if time()-start_time > interval:
queue.put("cake[%d]"%num)
state="free"
yield
continue
def consumer(name, interval):
state = "free"
while True:
if state == "free":
if not queue.empty():
cake = queue.get()
start_time = time()
state = "busy"
else:
print "no cake exists, %s is waiting"%name
yield
continue
if state == "busy":
if time()-start_time > interval:
print "%s finished eating %s"%(name,cake)
state = "free"
yield
continue
sister=consumer("sister",2)
brother=consumer("brother",3)
mom = producer("mom",1)
while True:
mom.next()
brother.next()
sister.next()
我们再做下改进,让它看上去更thread-like,也就是不要代码里出现上面这样的while True,而用start()
from Queue import Queue
from time import time
class Routine(object):
def __init__(self, name, target=None, parent=None):
self.name=name
self.parent=parent
self.children=[]
self.target=target
def add(self, child):
self.children.append(child)
def next(self):
if self.target:
return self.target.next()
def start(self):
if self.parent:
self.parent.add(self)
else:
while True:
for i in self.children:
i.next()
def producer(name, interval):
num=0
state="free"
while True:
if state=="free":
if not queue.full():
state="busy"
start_time=time()
print "%s is making cake[%d]"%(name, num)
num+=1
else:
print "more than 10 cakes exists, %s is waiting"%name
yield
continue
if state=="busy":
if time()-start_time > interval:
queue.put("cake[%d]"%num)
state="free"
yield
continue
def consumer(name, interval):
state = "free"
while True:
if state == "free":
if not queue.empty():
cake = queue.get()
start_time = time()
state = "busy"
else:
print "no cake exists, %s is waiting"%name
yield
continue
if state == "busy":
if time()-start_time > interval:
print "%s finished eating %s"%(name,cake)
state = "free"
yield
continue
queue=Queue(10)
sister=consumer("sister",2)
brother=consumer("brother",3)
mom = producer("mom",1)
main_routine=Routine(__name__)
routineA=Routine("A",sister,main_routine)
routineB=Routine("B",brother,main_routine)
routineC=Routine("C",mom,main_routine)
routineA.start()
routineB.start()
routineC.start()
main_routine.start()
上面的改进我们已经对协程的实现进行了一定的封装,但是还是有些不足;比如它需要自己去定义生成器;当然如果以“自己实现的协程”里的那种方式进行封装是不需要生成器的;那么有没有别人已经写好的封装呢?或者有没有更好用的协程库呢?因为我们需要让专业的人来做专业的事,自己造车多数情况下都是吃力不讨好的,需要大量的测试和优化,没有一个社区的力量是行不通的;
利用第三方库实现的协程
下面代码转自:
from gevent import monkey; monkey.patch_all()
import gevent
import urllib2
def f(url):
print('GET: %s' % url)
resp = urllib2.urlopen(url)
data = resp.read()
print('%d bytes received from %s.' % (len(data), url))
gevent.joinall([
gevent.spawn(f, 'https://www.python.org/'),
gevent.spawn(f, 'https://www.yahoo.com/'),
gevent.spawn(f, 'https://github.com/'),
])
它的输出如下:
GET: https://www.python.org/
GET: https://www.yahoo.com/
GET: https://github.com/
45661 bytes received from https://www.python.org/.
14823 bytes received from https://github.com/.
304034 bytes received from https://www.yahoo.com/.
可以看到,当遇到阻塞(如IO操作,或者gevent.sleep(0)),gevent的调度器会自动切换协程去执行;而且它的协程都是一个个的函数,这样就很nice了:看上去像线程,其实是协程。