Stackless Python并发式编程介绍[已校对版]

Stackless Python并发式编程介绍

作者: Grant Olson

作者:    Grant Olson
电子邮件:    olsongt@verizon.net
日期:    2006-07-07
译者:    刘禹呈
电子邮件:    lych77@gmail.com
日期:    2007-09-19
校对:    gashero
电子邮件:    harry.python@gmail.com
日期:    2007-09-20
原文地址:    http://members.verizon.net/olsongt/stackless/why_stackless.html
目录

1 介绍

1.1 为什么要使用Stackless

摘自 stackless 网站。

Note

Stackless Python 是Python编程语言的一个增强版本,它使程序员从基于线程的编程方式中获得好处,并避免传统线程所带来的性能与复杂度问题。Stackless为 Python带来的微线程扩展,是一种低开销、轻量级的便利工具,如果使用得当,可以获益如下:

  • 改进程序结构
  • 增进代码可读性
  • 提高编程人员生产力

以上是Stackless Python很简明的释义,但其对我们意义何在?——就在于Stackless提供的并发建模工具,比目前其它大多数传统编程语言所提供的,都更加易用: 不仅是Python自身,也包括Java、C++,以及其它。尽管还有其他一些语言提供并发特性,可它们要么是主要用于学术研究的(如 Mozart/Oz),要么是罕为使用、或用于特殊目的的专业语言(如Erlang)。而使用stackless,你将会在Python本身的所有优势之 上,在一个(但愿)你已经很熟悉的环境中,再获得并发的特性。

这自然引出了个问题:为什么要并发?

1.1.1 现实世界就是并发的

现实世界就是“并发”的,它是由一群事物(或“演员”)所组成,而这些事物以一种对彼此所知有限的、松散耦合的方式相互作用。传说中面向对象编程有 一个好处,就是对象能够对现实的世界进行模拟。这在一定程度上是正确的,面向对象编程很好地模拟了对象个体,但对于这些对象个体之间的交互,却无法以一种 理想的方式来表现。例如,如下代码实例,有什么问题?

def familyTacoNight():
    husband.eat(dinner)
    wife.eat(dinner)
    son.eat(dinner)
    daughter.eat(dinner)

第一印象,没问题。但是,上例中存在一个微妙的安排:所有事件是次序发生的,即:直到丈夫吃完饭,妻子才开始吃;儿子则一直等到母亲吃完才吃;而女 儿则是最后一个。在现实世界中,哪怕是丈夫还堵车在路上,妻子、儿子和女儿仍然可以该吃就吃,而要在上例中的话,他们只能饿死了——甚至更糟:永远没有人 会知道这件事,因为他们永远不会有机会抛出一个异常来通知这个世界!

1.1.2 并发可能是(仅仅可能是)下一个重要的编程范式

我个人相信,并发将是软件世界里的下一个重要范式。随着程序变得更加复杂和耗费资源,我们已经不能指望摩尔定律来每年给我们提供更快的CPU了,当 前,日常使用的个人计算机的性能提升来自于多核与多CPU机。一旦单个CPU的性能达到极限,软件开发者们将不得不转向分布式模型,靠多台计算机的互相协 作来建立强大的应用(想想GooglePlex)。为了取得多核机和分布式编程的优势,并发将很快成为做事情的方式的事实标准。

1.2 安装stackless

安装Stackless的细节可以在其网站上找到。现在Linux用户可以通过SubVersion取得源代码并编译;而对于Windows用户, 则有一个.zip文件供使用,需要将其解压到现有的Python安装目录中。接下来,本教程假设Stackless Python已经安装好了,可以工作,并且假设你对Python语言本身有基本的了解。

2 stackless起步

本章简要介绍了 stackless 的基本概念,后面章节将基于这些基础,来展示更加实用的功能。

2.1 微进程(tasklet)

微进程是stackless的基本构成单元,你可以通过提供任一个Python可调用对象(通常为函数或类的方法)来建立它,这将建立一个微进程并将其添加到调度器。这是一个快速演示:

Python 2.4.3 Stackless 3.1b3 060504 (#69, May  3 2006, 19:20:41) [MSC v.1310 32
bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import stackless
>>>
>>> def print_x(x):
...     print x
...
>>> stackless.tasklet(print_x)('one')
<stackless.tasklet object at 0x00A45870>
>>> stackless.tasklet(print_x)('two')
<stackless.tasklet object at 0x00A45A30>
>>> stackless.tasklet(print_x)('three')
<stackless.tasklet object at 0x00A45AB0>
>>>
>>> stackless.run()
one
two
three
>>>

注意,微进程将排起队来,并不运行,直到调用 stackless.run()

2.2 调度器(scheduler)

调度器控制各个微进程运行的顺序。如果刚刚建立了一组微进程,它们将按照建立的顺序来执行。在现实中,一般会建立一组可以再次被调度的微进程,好让每个都有轮次机会。一个快速演示:

Python 2.4.3 Stackless 3.1b3 060504 (#69, May  3 2006, 19:20:41) [MSC v.1310 32
bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import stackless
>>>
>>> def print_three_times(x):
...     print "1:", x
...     stackless.schedule()
...     print "2:", x
...     stackless.schedule()
...     print "3:", x
...     stackless.schedule()
...
>>>
>>> stackless.tasklet(print_three_times)('first')
<stackless.tasklet object at 0x00A45870>
>>> stackless.tasklet(print_three_times)('second')
<stackless.tasklet object at 0x00A45A30>
>>> stackless.tasklet(print_three_times)('third')
<stackless.tasklet object at 0x00A45AB0>
>>>
>>> stackless.run()
1: first
1: second
1: third
2: first
2: second
2: third
3: first
3: second
3: third
>>>

注意:当调用 stackless.schedule() 的时候,当前活动微进程将暂停执行,并将自身重新插入到调度器队列的末尾,好让下一个微进程被执行。一旦在它前面的所有其他微进程都运行过了,它将从上次 停止的地方继续开始运行。这个过程会持续,直到所有的活动微进程都完成了运行过程。这就是使用stackless达到合作式多任务的方式。

2.3 通道(channel)

通道使得微进程之间的信息传递成为可能。它做到了两件事:

  1. 能够在微进程之间交换信息。
  2. 能够控制运行的流程。

又一个快速演示:

C:>c:python24python
Python 2.4.3 Stackless 3.1b3 060504 (#69, May  3 2006, 19:20:41) [MSC v.1310 32
bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import stackless
>>>
>>> channel = stackless.channel()
>>>
>>> def receiving_tasklet():
...     print "Recieving tasklet started"
...     print channel.receive()
...     print "Receiving tasklet finished"
...
>>> def sending_tasklet():
...     print "Sending tasklet started"
...     channel.send("send from sending_tasklet")
...     print "sending tasklet finished"
...
>>> def another_tasklet():
...     print "Just another tasklet in the scheduler"
...
>>> stackless.tasklet(receiving_tasklet)()
<stackless.tasklet object at 0x00A45B30>
>>> stackless.tasklet(sending_tasklet)()
<stackless.tasklet object at 0x00A45B70>
>>> stackless.tasklet(another_tasklet)()
<stackless.tasklet object at 0x00A45BF0>
>>>
>>> stackless.run()
Recieving tasklet started
Sending tasklet started
send from sending_tasklet
Receiving tasklet finished
Just another tasklet in the scheduler
sending tasklet finished
>>>

接收的微进程调用 channel.receive() 的时候,便阻塞住,这意味着该微进程暂停执行,直到有信息从这个通道送过来。除了往这个通道发送信息以外,没有其他任何方式可以让这个微进程恢复运行。

若有其他微进程向这个通道发送了信息,则不管当前的调度到了哪里,这个接收的微进程都立即恢复执行;而发送信息的微进程则被转移到调度列表的末尾,就像调用了 stackless.schedule() 一样。

同样注意,发送信息的时候,若当时没有微进程正在这个通道上接收,也会使当前微进程阻塞:

>>> stackless.tasklet(sending_tasklet)()
<stackless.tasklet object at 0x00A45B70>
>>> stackless.tasklet(another_tasklet)()
<stackless.tasklet object at 0x00A45BF0>
>>>
>>> stackless.run()
Sending tasklet started
Just another tasklet in the scheduler
>>>
>>> stackless.tasklet(another_tasklet)()
<stackless.tasklet object at 0x00A45B30>
>>> stackless.run()
Just another tasklet in the scheduler
>>>
>>> #Finally adding the receiving tasklet
...
>>> stackless.tasklet(receiving_tasklet)()
<stackless.tasklet object at 0x00A45BF0>
>>>
>>> stackless.run()
Recieving tasklet started
send from sending_tasklet
Receiving tasklet finished
sending tasklet finished

发送信息的微进程,只有在成功地将数据发送到了另一个微进程之后,才会重新被插入到调度器中。

2.4 总结

以上涵盖了stackless的大部分功能。似乎不多是吧?——我们只使用了少许对象,和大约四五个函数调用,来进行操作。但是,使用这种简单的API作为基本建造单元,我们可以开始做一些真正有趣的事情。

3 协程(coroutine)

3.1 子例程的问题

大多数传统编程语言具有子例程的概念。一个子例程被另一个例程(可能还是其它某个例程的子例程)所调用,或返回一个结果,或不返回结果。从定义上说,一个子例程是从属于其调用者的。

见下例:

def ping():
    print "PING"
    pong()

def pong():
    print "PONG"
    ping()

ping()

有经验的编程者会看到这个程序的问题所在:它导致了堆栈溢出。如果运行这个程序,它将显示一大堆讨厌的跟踪信息,来指出堆栈空间已经耗尽。

3.1.1 堆栈

我仔细考虑了,自己对C语言堆栈的细节究竟了解多少,最终还是决定完全不去讲它。似乎,其他人对其所尝试的描述,以及图表,只有本身已经理解了的人才能看得懂。我将试着给出一个最简单的说明,而对其有更多兴趣的读者可以从网上查找更多信息。

每当一个子例程被调用,都有一个“栈帧”被建立,这是用来保存变量,以及其他子例程局部信息的区域。于是,当你调用 ping() ,则有一个栈帧被建立,来保存这次调用相关的信息。简言之,这个帧记载着 ping 被调用了。当再调用 pong() ,则又建立了一个栈帧,记载着 pong 也被调用了。这些栈帧是串联在一起的,每个子例程调用都是其中的一环。就这样,堆栈中显示: ping 被调用所以 pong 接下来被调用。显然,当 pong() 再调用 ping() ,则使堆栈再扩展。下面是个直观的表示:

堆栈
1 ping 被调用
2 ping 被调用,所以 pong 被调用
3 ping 被调用,所以 pong 被调用,所以 ping 被调用
4 ping 被调用,所以 pong 被调用,所以 ping 被调用,所以 pong 被调用
5 ping 被调用,所以 pong 被调用,所以 ping 被调用,所以 pong 被调用,所以 ping 被调用
6 ping 被调用,所以 pong 被调用,所以 ping 被调用,所以 pong 被调用,所以 ping 被调用……

现在假设,这个页面的宽度就表示系统为堆栈所分配的全部内存空间,当其顶到页面的边缘的时候,将会发生溢出,系统内存耗尽,即术语“堆栈溢出”。

3.1.2 那么,为什么要使用堆栈?

上例是有意设计的,用来体现堆栈的问题所在。在大多数情况下,当每个子例程返回的时候,其栈帧将被清除掉,就是说堆栈将会自行实现清理过程。这一般 来说是件好事,在C语言中,堆栈就是一个不需要编程者来手动进行内存管理的区域。很幸运,Python程序员也不需要直接来担心内存管理与堆栈。但是由于 Python解释器本身也是用C实现的,那些实现者们可是需要担心这个的。使用堆栈是会使事情方便,除非我们开始调用那种从不返回的函数,如上例中的,那 时候,堆栈的表现就开始和程序员别扭起来,并耗尽可用的内存。

3.2 走进协程

此时,将堆栈弄溢出是有点愚蠢的。 ping() 和 pong() 本不是真正意义的子例程,因为其中哪个也不从属于另一个,它们是“协程”,处于同等的地位,并可以彼此间进行无缝通信。

堆栈
1 ping 被调用
2 pong 被调用
3 ping 被调用
4 pong 被调用
5 ping 被调用
6 pong 被调用

在stackless中,我们使用通道来建立协程。还记得吗,通道所带来的两个好处中的一个,就是能够控制微进程之间运行的流程。使用通道,我们可以在 ping 和 pong 这两个协程之间自由来回,要多少次就多少次,都不会堆栈溢出:

#
# pingpong_stackless.py
#

import stackless

ping_channel = stackless.channel()
pong_channel = stackless.channel()

def ping():
    while ping_channel.receive(): #在此阻塞
        print "PING"
        pong_channel.send("from ping")

def pong():
    while pong_channel.receive():
        print "PONG"
        ping_channel.send("from pong")

stackless.tasklet(ping)()
stackless.tasklet(pong)()

# 我们需要发送一个消息来初始化这个游戏的状态
# 否则,两个微进程都会阻塞
stackless.tasklet(ping_channel.send)('startup')

stackless.run()

你可以运行这个程序要多久有多久,它都不会崩溃,且如果你检查其内存使用量(使用Windows的任务管理器或Linux的top命令),将会发现 使用量是恒定的。这个程序的协程版本,不管运行一分钟还是一天,使用的内存都是一样的。而如果你检查原先那个递归版本的内存用量,则会发现其迅速增长,直 到崩溃。

3.3 总结

是否还记得,先前我提到过,那个代码的递归版本,有经验的程序员会一眼看出毛病。但老实说,这里面并没有什么“计算机科学”方面的原因在阻碍它的正 常工作,有些让人坚信的东西,其实只是个与实现细节有关的小问题——只因为大多数传统编程语言都使用堆栈。某种意义上说,有经验的程序员都是被洗了脑,从 而相信这是个可以接受的问题。而stackless,则真正察觉了这个问题,并除掉了它。

4 轻量级线程

与当今的操作系统中内建的、和标准Python代码中所支持的普通线程相比,“微线程”要更为轻量级,正如其名称所暗示。它比传统线程占用更少的内存,并且微线程之间的切换,要比传统线程之间的切换更加节省资源。

为了准确说明微线程的效率究竟比传统线程高多少,我们用两者来写同一个程序。

4.1 hackysack模拟

Hackysack是一种游戏,就是一伙脏乎乎的小子围成一个圈,来回踢一个装满了豆粒的沙包,目标是不让这个沙包落地,当传球给别人的时候,可以耍各种把戏。踢沙包只可以用脚。

在我们的简易模拟中,我们假设一旦游戏开始,圈里人数就是恒定的,并且每个人都是如此厉害,以至于如果允许的话,这个游戏可以永远停不下来。

4.2 游戏的传统线程版本

import thread
import random
import sys
import Queue

class hackysacker:
    counter = 0
    def __init__(self,name,circle):
        self.name = name
        self.circle = circle
        circle.append(self)
        self.messageQueue = Queue.Queue()

        thread.start_new_thread(self.messageLoop,())

    def incrementCounter(self):
        hackysacker.counter += 1
        if hackysacker.counter >= turns:
            while self.circle:
                hs = self.circle.pop()
                if hs is not self:
                    hs.messageQueue.put('exit')
            sys.exit()

    def messageLoop(self):
        while 1:
            message = self.messageQueue.get()
            if message == "exit":
                debugPrint("%s is going home" % self.name)
                sys.exit()
            debugPrint("%s got hackeysack from %s" % (self.name, message.name))
            kickTo = self.circle[random.randint(0,len(self.circle)-1)]
            debugPrint("%s kicking hackeysack to %s" % (self.name, kickTo.name))
            self.incrementCounter()
            kickTo.messageQueue.put(self)

def debugPrint(x):
    if debug:
        print x

debug=1
hackysackers=5
turns = 5

def runit(hs=10,ts=10,dbg=1):
    global hackysackers,turns,debug
    hackysackers = hs
    turns = ts
    debug = dbg

    hackysacker.counter= 0
    circle = []
    one = hackysacker('1',circle)

    for i in range(hackysackers):
        hackysacker(`i`,circle)

    one.messageQueue.put(one)

    try:
        while circle:
            pass
    except:
        #有时我们在清理过程中会遇到诡异的错误。
        pass

if __name__ == "__main__":
    runit(dbg=1)

一个“玩者”类的初始化用到了其名字,和一个指向包含了所有玩者的全局列表 circle 的引用,还有一个继承自Python标准库中的Queue类的消息队列。

Queue这个类的作用,与stackless的通道类似。它包含 put() 和 get() 方法,在一个空的Queue上调用 put() 会阻塞,直到另一个线程调用 put() 将数据送入Queue中为止。Queue这个类被设计为能与操作系统级的线程高效合作。

__init__ 方法接下来使用Python标准库中的thread模块新建一个线程,并在新线程中开始了一个消息循环。此消息循环是个无限循环,不停地处理队列中的消息。如果其收到一个特殊的消息 ‘exit’ ,则结束这个线程。

如果收到了另一个消息——指定其收到了沙包,玩者则从圈中随机选取一个其他玩者,通过向其发送一条消息来指定,将沙包再踢给它。

由类成员变量 hackysacker.counter 进行计数,当沙包被踢够了指定的次数时,将会向圈中的所有玩者都发送一条特殊的 ‘exit’ 消息。

注意,当全局变量debug为非零的时候,还有个函数debugPrint可以输出信息。我们可以使这游戏输出到标准输出,但当计时的时候,这会影响精确度。

我们来运行这个程序,并检查其是否正常工作:

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:python24python.exe
hackysackthreaded.py

1 got hackeysack from 1
1 kicking hackeysack to 4
4 got hackeysack from 1
4 kicking hackeysack to 0
0 got hackeysack from 4
0 kicking hackeysack to 1
1 got hackeysack from 0
1 kicking hackeysack to 3
3 got hackeysack from 1
3 kicking hackeysack to 3
4 is going home
2 is going home
1 is going home
0 is going home
1 is going home

C:Documents and SettingsgrantDesktopwhy_stacklesscode>

如我们所见,所有玩者到了一起,并很快地进行了一场游戏。现在,我们对若干次实验运行过程进行计时。Python标准库中有一个 timeit.py 程序,可以用作此目的。那么,我们也同时关掉调试输出:

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:python24python.exe c:Python24libtimeit.py -s "import hackysackthreaded" hackysackthreaded.runit(10,1000,0)
10 loops, best of 3: 183 msec per loop

在我的机器上,十个玩者共进行1000次传球,共使用了183毫秒。我们来增加玩者的数量:

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:python24python.exe c:Python24libtimeit.py -s "import hackeysackthreaded" hackeysackthreaded.runit(100,1000,0)
10 loops, best of 3: 231 msec per loop

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:python24python.exe c:Python24libtimeit.py -s "import hackysackthreaded" hackysackthreaded.runit(1000,1000,0)
10 loops, best of 3: 681 msec per loop

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:python24python.exe c:Python24libtimeit.py -s "import hackysackthreaded" hackysackthreaded.runit(10000,1000,0)
Traceback (most recent call last):
  File "c:Python24libtimeit.py", line 255, in main
    x = t.timeit(number)
  File "c:Python24libtimeit.py", line 161, in timeit
    timing = self.inner(it, self.timer)
  File "<timeit-src>", line 6, in inner
  File ".hackeysackthreaded.py", line 58, in runit
    hackysacker(`i`,circle)
  File ".hackeysackthreaded.py", line 14, in __init__
    thread.start_new_thread(self.messageLoop,())
error: can't start new thread

在我的3GHz、1G内存的机器上,当尝试10,000个线程的时候出现了错误。就不想拿出这详细的输出内容来扰人了,只是通过若干实验与出错过程 得出,在我机器上,此程序从1100个线程左右开始出错。另请注意,1000个线程时候所耗用的时间,是10个线程时候的大约三倍。

4.3 stackless

import stackless
import random
import sys

class hackysacker:
    counter = 0
    def __init__(self,name,circle):
        self.name = name
        self.circle = circle
        circle.append(self)
        self.channel = stackless.channel()

        stackless.tasklet(self.messageLoop)()

    def incrementCounter(self):
        hackysacker.counter += 1
        if hackysacker.counter >= turns:
            while self.circle:
                self.circle.pop().channel.send('exit')

    def messageLoop(self):
        while 1:
            message = self.channel.receive()
            if message == 'exit':
                return
            debugPrint("%s got hackeysack from %s" % (self.name, message.name))
            kickTo = self.circle[random.randint(0,len(self.circle)-1)]
            while kickTo is self:
                kickTo = self.circle[random.randint(0,len(self.circle)-1)]
            debugPrint("%s kicking hackeysack to %s" % (self.name, kickTo.name))
            self.incrementCounter()
            kickTo.channel.send(self)

def debugPrint(x):
    if debug:print x

debug = 5
hackysackers = 5
turns = 1

def runit(hs=5,ts=5,dbg=1):
    global hackysackers,turns,debug
    hackysackers = hs
    turns = ts
    debug = dbg

    hackysacker.counter = 0
    circle = []
    one = hackysacker('1',circle)

    for i in range(hackysackers):
        hackysacker(`i`,circle)

    one.channel.send(one)

    try:
        stackless.run()
    except TaskletExit:
        pass

if __name__ == "__main__":
    runit()

以上代码实质上与线程版本是等价的,主要区别仅在于我们使用微进程来代替线程,并且使用通道代替Queue来进行切换。让我们运行它,并检查输出:

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:Python24python.exe hackysackstackless.py
1 got hackeysack from 1
1 kicking hackeysack to 1
1 got hackeysack from 1
1 kicking hackeysack to 4
4 got hackeysack from 1
4 kicking hackeysack to 1
1 got hackeysack from 4
1 kicking hackeysack to 4
4 got hackeysack from 1
4 kicking hackeysack to 0

工作情况确如所料。现在来计时:

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:Python24python.exe c:Python24libtimeit.py -s"import hackysackstackless" hackysackstackless.runit(10,1000,0)
100 loops, best of 3: 19.7 msec per loop

其仅用了19.7毫秒,速度几乎是线程版本的10倍。现在我们同样开始增加微线程的数量:

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:Python24python.exe c:Python24libtimeit.py -s"import hackysackstackless" hackysackstackless.runit(100,1000,0)
100 loops, best of 3: 19.7 msec per loop

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:Python24python.exe c:Python24libtimeit.py -s"import hackysackstackless" hackysackstackless.runit(1000,1000,0)
10 loops, best of 3: 26.9 msec per loop

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:Python24python.exe c:Python24libtimeit.py -s"import hackysackstackless" hackysackstackless.runit(10000,1000,0)
10 loops, best of 3: 109 msec per loop

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:Python24python.exe c:Python24libtimeit.py -s"import hackysackstackless" hackysackstackless.runit(100000,1000,0)
10 loops, best of 3: 1.07 sec per loop

甚至直到10,000个线程的时候,那时线程版本早已不能运行了,而这个仍然可以比线程版本在10个线程的时候运行的还快。

这里我在尽量保持代码的简洁,因此你可以相信我的话:计时时间的增长仅仅在于初始化游戏圈子的部分,而真正进行游戏的时间则是一直不变的,不管使用 10个微线程,还是10,000个。这归因于通道的工作方式:当它们收到消息的时候,是立即进行阻塞和恢复操作的。另一方面,各个操作系统线程则是轮番检 查自己的队列里是否有了东西,这意味着,跑着越多的线程,性能就变得越差。

4.4 总结

但愿我已经成功地演示了,微线程的运行至少比操作系统线程快一个数量级,并具备远高于后者的可伸缩性。关于操作系统线程的一般常识是:(1)尽量不要使用它,(2)如果非用不可,就能少用一点就少用一点。而stackless的微线程则使我们从这些限制中解放出来。

5 数据流

5.1 工厂

假设,我们要写程序来模拟一个生产玩具娃娃的工厂,具有如下的需求:

  • 一个仓库,装有用来塑造的塑料球。
  • 一个仓库,装有用来连接部件的铆钉。
  • 一台注塑机,可以在6秒内,用0.2磅塑料球来制造一双手臂。
  • 一台注塑机,可以在5秒内,用0.2磅塑料球来制造一双腿。
  • 一台注塑机,可以在4秒内,用0.1磅塑料球来制造一个头部。
  • 一台注塑机,可以在10秒内,用0.5磅塑料球来制造一个躯干。
  • 一个装配台,可以在2秒内,将一个现成的躯干和一双现成的腿,用一个铆钉装配在一起。
  • 一个装配台,可以在2秒内,将上面的半成品和一双现成的手臂,用一个铆钉装配在一起。
  • 一个装配台,可以在3秒内,将上面的半成品和一个现成的头部,用一个铆钉装配在一起。
  • 每台设备都一直不停地工作下去。

5.2 “普通”版本

如果不用stackless而用“普通”的方法来写这个,将会是很痛苦的事情。当我们经历了这个“普通”版示例之后,会再用stackless来做 一个,并比较两者的代码。如果你认为这个例子太不自然,并且有时间的话,可以稍为休息后,根据上面的需求,自己来做一个工厂的实现,再来将你写出的代码和 stackless版本做个比较。

代码如下:

class storeroom:
    def __init__(self,name,product,unit,count):
        self.product = product
        self.unit = unit
        self.count = count
        self.name = name

    def get(self,count):
        if count > self.count:
            raise RuntimeError("Not enough %s" % self.product)
        else:
            self.count -= count

        return count

    def put(self,count):
        self.count += count

    def run(self):
        pass

rivetStoreroom = storeroom("rivetStoreroom","rivets","#",1000)
plasticStoreroom = storeroom("plastic Storeroom","plastic pellets","lb",100)

class injectionMolder:
    def __init__(self,name,partName,plasticSource,plasticPerPart,timeToMold):
        self.partName = partName
        self.plasticSource = plasticSource
        self.plasticPerPart = plasticPerPart
        self.timeToMold = timeToMold
    self.items = 0
    self.plastic = 0
    self.time = -1
    self.name = name

def get(self,items):
    if items > self.items:
        return 0
    else:
        self.items -= items
        return items

def run(self):
    if self.time == 0:
        self.items += 1
        print "%s finished making part" % self.name
        self.time -= 1
    elif self.time < 0:
        print "%s starts making new part %s" % (self.name,self.partName)
        if self.plastic < self.plasticPerPart:
            print "%s getting more plastic"
            self.plastic += self.plasticSource.get(self.plasticPerPart * 10)
        self.time = self.timeToMold
    else:
        print "%s molding for %s more seconds" % (self.partName, self.time)
        self.time -= 1

armMolder = injectionMolder("arm Molder", "arms",plasticStoreroom,0.2,6)
legMolder = injectionMolder("leg Molder", "leg",plasticStoreroom,0.2,5)
headMolder = injectionMolder("head Molder","head",plasticStoreroom,0.1,4)
torsoMolder = injectionMolder("torso Molder","torso",plasticStoreroom,0.5,10)

class assembler:
    def __init__(self,name,partAsource,partBsource,rivetSource,timeToAssemble):
        self.partAsource = partAsource
        self.partBsource = partBsource
        self.rivetSource = rivetSource
        self.timeToAssemble = timeToAssemble
        self.itemA = 0
        self.itemB = 0
        self.items = 0
        self.rivets = 0
        self.time = -1
        self.name = name

    def get(self,items):
        if items > self.items:
            return 0
        else:
            self.items -= items
            return items

    def run(self):
        if self.time == 0:
            self.items += 1
            print "%s finished assembling part" % self.name
            self.time -= 1
        elif self.time < 0:
            print "%s starts assembling new part" % self.name
            if self.itemA < 1:
                print "%s Getting item A" % self.name
                self.itemA += self.partAsource.get(1)
                if self.itemA < 1:
                    print "%s waiting for item A" % self.name
            elif self.itemB < 1:
                print "%s Getting item B" % self.name
                self.itemB += self.partBsource.get(1)
                if self.itemB < 1:
                    print "%s waiting for item B" % self.name
            print "%s starting to assemble" % self.name
            self.time = self.timeToAssemble
        else:
            print "%s assembling for %s more seconds" % (self.name, self.time)
            self.time -= 1

legAssembler = assembler("leg Assembler",torsoMolder,legMolder,rivetStoreroom,2)
armAssembler = assembler("arm Assembler", armMolder,legAssembler,rivetStoreroom,2)
torsoAssembler = assembler("torso Assembler", headMolder,armAssembler,
                            rivetStoreroom,3)

components = [rivetStoreroom, plasticStoreroom, armMolder,
                legMolder, headMolder, torsoMolder,
              legAssembler, armAssembler, torsoAssembler]

def run():
    while 1:
        for component in components:
            component.run()
        raw_input("Press <ENTER> to continue...")
        print "nnn"

if __name__ == "__main__":
    run()

5.2.1 分析

我们从一个代表仓库的类开始,它的初始化需要一个其所储存的产品的名称、一个衡量单位(如磅,或部件数目)和一个初始存量作为参数。还有一个 run 方法什么也不做,其用途将会在稍后了解。基于这个类,我们建立了两个仓库示例。

接下来是一个注塑机类,它的初始化需要其产品的名称、一个作为塑料来源的仓库、制造一个部件所需要的原料量,和制造一个部件所需的时间作为参数。有 一个 get() 方法,在其内部已有完成的产品时,可将其取出,并调整内部记录。对于这个类, run() 方法是确实做了些事情的:

  • 在计时器大于0期间,塑造过程持续进行,并递减计时器。
  • 当塑造剩余时间达到0,则一个产品被建立,并把计时器设为-1。
  • 当计时器为-1时,注塑机检测是否还有足够的塑料来塑造下一个产品,如果有,则取来原料,并开始塑造。

用这个类,我们建立了四个注塑机实例。

再接下来是一个装配台类,它的初始化需要其产品的名字、部件1的来源、部件2的来源、一个铆钉的仓库,以及装配这些部件所需的时间作为参数。也有一个 get() 方法,在其内部已有完成的产品时,可将其取出,并调整内部记录。而这个类的 run() 方法是这样的:

  • 若计时器大于0,则已经具备原材料的装配台继续其装配过程。
  • 如果计时器等于0,则一个产品被完成,内部记录随之被调整。
  • 如果计时器小于0,则装配台试图取得新的各个部件,并再次开始装配。若其中某个部件还没有来得及塑造出来,则必须等待。

为了装配腿、手臂和头部,各有一个装配台实例被建立。

Note

你会注意到,仓库、注塑机和装配台类有很多相似之处。如果我是在写一个真正生产系统,则很可能先建立一个基类,并使用继承。但在这里,我觉得做出这种类层次关系的话只会使代码变得繁杂,所以有意保持了其简单。

由以上三个类所建立的所有实例,都被装进一个称为 components 的“设备”数组中。然后,我们建立一个事件循环,重复地调用每个设备的 run() 方法。

5.3 走进数据流

如果你熟悉 Unix 系统,那么不管你知不知道数据流技术,恐怕你都已经在使用它了。看下面的 shell 命令:

cat README | more

为了公平,也举出 Windows 中对应的:

type readme.txt | more

尽管,在 Windows 的世界中,数据流技术并不像在 Unix 世界中那么普遍深入。

顺便对还不熟悉 more 工具的读者:这个程序从一个外部来源接收输入,显示一页的内容后暂停,直到用户按下任意键,再显示下一页。这个“|”操作符获取一个程序的输出,并用管道 将其传送到另一个命令的输入。这样,不管 cat 还是 type ,都是将文档内容传送到标准输出,而 more 则接收这些输出。

这样,more 程序仅仅是坐在那里,等着来自另一个程序的数据来流向自己。只要流进的数据足够一定量,就在屏幕上显示一页并暂停;而用户击键时,more 则让后面的数据再流入,并开始再一次等待数据量足够,再显示,再暂停。这便是术语“数据流”。

使用通道,再使用stackless本身的轮转调度器,我们就可以使用数据流技术来写这个工厂的模拟。

5.4 代码的stackless版本

import stackless

#
# “休眠” 辅助函数
#

sleepingTasklets = []
sleepingTicks = 0

def Sleep(secondsToWait):
    channel = stackless.channel()
    endTime = sleepingTicks + secondsToWait
    sleepingTasklets.append((endTime, channel))
    sleepingTasklets.sort()
    # 阻塞,直到收到一个唤醒通知。
    channel.receive()

def ManageSleepingTasklets():
    global sleepingTicks
    while 1:
        if len(sleepingTasklets):
            endTime = sleepingTasklets[0][0]
            while endTime <= sleepingTicks:
                channel = sleepingTasklets[0][1]
                del sleepingTasklets[0]
                # 我们需要发送一些东西,但发什么无所谓,
                # 因为其内容是没用的。
                channel.send(None)
                endTime = sleepingTasklets[0][0] # 检查下一个
        sleepingTicks += 1
        print "1 second passed"
        stackless.schedule()

stackless.tasklet(ManageSleepingTasklets)()

#
# 工厂的实现
#

class storeroom:
    def __init__(self,name,product,unit,count):
        self.product = product
        self.unit = unit
        self.count = count
        self.name = name

    def get(self,count):
        while count > self.count: #重新调度,直到有了足够的原料
            print "%s doesn't have enough %s to deliver yet" % (self.name,
                                                                self.product)
            stackless.schedule()
        self.count -= count
        return count

        return count

    def put(self,count):
        self.count += count

    def run(self):
        pass

rivetStoreroom = storeroom("rivetStoreroom","rivets","#",1000)
plasticStoreroom = storeroom("plastic Storeroom","plastic pellets","lb",100)

class injectionMolder:
    def __init__(self,name,partName,plasticSource,plasticPerPart,timeToMold):
        self.partName = partName
        self.plasticSource = plasticSource
        self.plasticPerPart = plasticPerPart
        self.timeToMold = timeToMold
        self.plastic = 0
        self.items = 0
        self.name = name
        stackless.tasklet(self.run)()

    def get(self,items):
        while items > self.items: #重新调度,直到有了足够的产品
            print "%s doesn't have enough %s to deliver yet" % (self.name,
                                                                self.partName)
            stackless.schedule()
        self.items -= items
        return items

    def run(self):
        while 1:
            print "%s starts making new part %s" % (self.name,self.partName)
            if self.plastic < self.plasticPerPart:
                print "%s getting more plastic"
                self.plastic += self.plasticSource.get(self.plasticPerPart * 10)
            self.plastic -= self.plasticPerPart
            Sleep(self.timeToMold)
            print "%s done molding after %s seconds" % (self.partName,
                                                        self.timeToMold)
            self.items += 1
            print "%s finished making part" % self.name
            stackless.schedule()

armMolder = injectionMolder("arm Molder", "arms",plasticStoreroom,0.2,5)
legMolder = injectionMolder("leg Molder", "leg",plasticStoreroom,0.2,5)
headMolder = injectionMolder("head Molder","head",plasticStoreroom,0.1,5)
torsoMolder = injectionMolder("torso Molder","torso",plasticStoreroom,0.5,10)

class assembler:
    def __init__(self,name,partAsource,partBsource,rivetSource,timeToAssemble):
        self.partAsource = partAsource
        self.partBsource = partBsource
        self.rivetSource = rivetSource
        self.timeToAssemble = timeToAssemble
        self.itemA = 0
        self.itemB = 0
        self.items = 0
        self.rivets = 0
        self.name = name
        stackless.tasklet(self.run)()

    def get(self,items):
        while items > self.items: #重新调度,直到有了足够的产品
            print "Don't have a %s to deliver yet" % (self.name)
            stackless.schedule()
        self.items -= items
        return items

    def run(self):
        while 1:
            print "%s starts assembling new part" % self.name
            self.itemA += self.partAsource.get(1)
            self.itemB += self.partBsource.get(1)
            print "%s starting to assemble" % self.name
            Sleep(self.timeToAssemble)
            print "%s done assembling after %s" % (self.name, self.timeToAssemble)
            self.items += 1
            print "%s finished assembling part" % self.name
            stackless.schedule()

legAssembler = assembler("leg Assembler",torsoMolder,legMolder,rivetStoreroom,2)
armAssembler = assembler("arm Assembler", armMolder,legAssembler,rivetStoreroom,2)
torsoAssembler = assembler("torso Assembler", headMolder,armAssembler,
                            rivetStoreroom,3)

def pause():
    while 1:
        raw_input("Press <ENTER> to continue...")
        print "nnn"
        stackless.schedule()

stackless.tasklet(pause)()

def run():
    stackless.run()

if __name__ == "__main__":
    run()

5.4.1 分析

5.4.1.1 休眠功能

首先我们建立了一些辅助函数,好让我们的类可以进行“休眠”。一个微进程调用 Sleep() ,则先建立一个通道,再计算出将被唤醒的时间,并将这个时间信息添加到全局数组 sleepingTasklets 中。之后,将调用 channel.receive() ,这将使该微进程暂停运行,直到被再次唤醒。

接着我们建立另一个函数,来管理所有休眠的微进程。它检查全局数组 sleepingTasklets ,找出所有需要立即被唤醒的成员,并通过其通道来将其唤醒。这个函数也被添加到了微进程调度器中。

5.4.1.2 类

这些类与“普通”版本中的类相似,但也有一些显著不同:首先,在实例化的时候,他们的 run() 方法创建了微进程,这样我们不再需要手工建立一个设备数组,和一个外部的 run() 函数来处理事件循环,stackless本身就隐式地做了这些工作。其次的不同是,微进程可以通过休眠来等待一个产品被产出,而不用通过计数器来计时。第 三个不同,则是对 get() 的调用变得更自然了,如果某种原材料没有准备好,则这个微进程简单地重新进入调度循环,直到有了原材料。

5.5 那我们获得了什么?

OK,两个版本的程序都能运行,并得到同样的结果,那这里究竟有什么大不了的事情?——让我们查看一下普通版本的工厂的 run 方法:

def run(self):
    if self.time == 0:
        self.items += 1
        print "%s finished assembling part" % self.name
        self.time -= 1
    elif self.time < 0:
        print "%s starts assembling new part" % self.name
        if self.itemA < 1:
            print "%s Getting item A" % self.name
            self.itemA += self.partAsource.get(1)
            if self.itemA < 1:
                print "%s waiting for item A" % self.name
        elif self.itemB < 1:
            print "%s Getting item B" % self.name
            self.itemB += self.partBsource.get(1)
            if self.itemB < 1:
                print "%s waiting for item B" % self.name
        print "%s starting to assemble" % self.name
        self.time = self.timeToAssemble
    else:
        print "%s assembling for %s more seconds" % (self.name, self.time)
        self.time -= 1

再看 stackless 的版本:

def run(self):
    while 1:
        print "%s starts assembling new part" % self.name
        self.itemA += self.partAsource.get(1)
        self.itemB += self.partBsource.get(1)
        print "%s starting to assemble" % self.name
        Sleep(self.timeToAssemble)
        print "%s done assembling after %s" % (self.name, self.timeToAssemble)
        self.items += 1
        print "%s finished assembling part" % self.name
        stackless.schedule()

Stackless 的版本比普通的版本更加简单、清晰和直观,它不需要将事件循环的基础结构包装进 run 方法中,这个结构已经和 run() 方法解除了耦合。run() 方法仅仅描述了自己要做什么,而不需要关心具体究竟怎么做的。这就使软件开发者能集中精力于工厂的运作,而不是事件循环以及程序本身的运作。

5.6 推(push)数据

Note

本节的完整程序保存为 digitalCircuit.py ,在本文的末尾,和代码.zip文件中和都有。

在工厂的例子中,我们是在“拉”数据:每个部分都去请求其所需要的部件,并一直等待那些部件到来。我们也可以来“推”数据,这样,系统中的每个部分都将自身的变化向下传播到另一个部分。“拉”的方式,称为“ 懒惰数据流 ”,而“推”的方式则称为“ 急切数据流 ”。

为了演示“推”的方式,我们来建立一个数字电路的模拟器。这个模拟器由各种元件组成,元件具有0或1的状态,并可以各种方式互相连接起来。这里我们使用面向对象的方法,并定义一个 EventHandler 基类来实现其大部分功能:

class EventHandler:
    def __init__(self,*outputs):
        if outputs==None:
            self.outputs=[]
        else:
            self.outputs=list(outputs)

        self.channel = stackless.channel()
        stackless.tasklet(self.listen)()

    def listen(self):
        while 1:
            val = self.channel.receive()
            self.processMessage(val)
            for output in self.outputs:
                self.notify(output)

    def processMessage(self,val):
        pass

    def notify(self,output):
        pass

    def registerOutput(self,output):
        self.outputs.append(output)

    def __call__(self,val):
    self.channel.send(val)

EventHandler 类的核心功能,是做以下三件事:

  • 通过 listen 方法,持续地监听一个通道上传来的消息。
  • 之后,通过 processMessage 方法,处理所有收到的消息。
  • 最后,通过 notify 方法,将处理结果通知到所有注册的输出端。

还有两个附加的辅助方法:

  • registerOutput 用来在实例建立之后,再注册额外的输出端。
  • __call__ 被重载,作为一种便利,使我们可以以这种格式来发送消息:
    event(1)

从而无需这样:

event.channel.send(1)

使用 EventHandler 类作为基本构建单元,我们可以开始实现这个数字电路模拟器,由一个开关开始。下面描述的是一个可由用户控制的开关,可以向其发送0或1的值:

class Switch(EventHandler):
    def __init__(self,initialState=0,*outputs):
        EventHandler.__init__(self,*outputs)
        self.state = initialState

    def processMessage(self,val):
        debugPrint("Setting input to %s" % val)
        self.state = val

    def notify(self,output):
        output((self,self.state))

初始化之后,这个开关就保存着其初始的状态,而 processMessage 则被重载,用来将收到的消息保存起来,成为新的当前状态。其 notify 方法则被重载为发送一个元组,其中含有指向实例自身的引用,还有其状态。我们不久后会看到,我们需要顺便发送这个自身的引用,这样,那些具有多个输入端的 元件则可以判别,消息来自于哪个来源。

Note

若你正在随着我们的进度来调试代码,则别忘了我们还在使用 debugPrint() 函数来提供诊断信息,它最初是在“轻量级线程”这一节中定义的。

接下来我们要建立的,是“指示器”类,这个类的实例的作用仅仅是将其当前状态输出。我想我们可以认为,其相当于真正的数字电路中的发光二极管:

class Reporter(EventHandler):
    def __init__(self,msg="%(sender)s send message %(value)s"):
        EventHandler.__init__(self)
        self.msg = msg

    def processMessage(self,msg):
        sender,value=msg
        print self.msg % {'sender':sender,'value':value}

其初始化接受一个可选的格式字符串,来指定之后输出的样式。代码的其他部分意义自明。

现在我们有了一个足够好的框架,来测试这些最初的功能:

C:Documents and SettingsgrantDesktopwhy_stacklesscode>c:Python24python.ex
e
Python 2.4.3 Stackless 3.1b3 060516 (#69, May  3 2006, 11:46:11) [MSC v.1310 32
bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import stackless
>>> from digitalCircuit import *
>>>
>>> reporter = Reporter()
>>> switch = Switch(0,reporter) #创建一个开关,并连接到一个指示器做输出。
>>>
>>> switch(1)
<digitalCircuit.Switch instance at 0x00A46828> send message 1
>>>
>>> switch(0)
<digitalCircuit.Switch instance at 0x00A46828> send message 0
>>>
>>> switch(1)
<digitalCircuit.Switch instance at 0x00A46828> send message 1
>>>

与先前设计的工厂不同,对开关的操作会使结果立即被推至其输出端,并显示出来。

现在我们来建立一些数字逻辑部件,首先是反相器,它接受一个输入,并将其逻辑相反的值推出,就是说输入0会输出1,输入1会输出0:

class Inverter(EventHandler):
    def __init__(self,input,*outputs):
        EventHandler.__init__(self,*outputs)
        self.input = input
        input.registerOutput(self)
        self.state = 0

    def processMessage(self,msg):
        sender,value = msg
        debugPrint("Inverter received %s from %s" % (value,msg))
        if value:
            self.state = 0
        else:
            self.state = 1

反相器的初始化参数为一个输入端,即另外某个 EventHandler ,将它保存下来,并将自身注册为它的一个输出端。而 processMessage() 方法,则将自身的状态设为收到的消息的逻辑相反值。与 Switch 类类似,反相器类的 notify 事件也发送一个由其自身和其状态所组成的元组。

我们可以修改上面的例子,在开关和指示器之间串联入一个反相器。如有兴趣,尽可一试,但这个过程我认为已经没有必要列出了。

接下来是一个与门,这是我们遇到的第一个有多个输入端的类。它有两个输入端,如果都被置为1,则送出消息1,否则送出消息0:

class AndGate(EventHandler):
    def __init__(self,inputA,inputB,*outputs):
        EventHandler.__init__(self,*outputs)

        self.inputA = inputA
        self.inputAstate = inputA.state
        inputA.registerOutput(self)

        self.inputB = inputB
        self.inputBstate = inputB.state
        inputB.registerOutput(self)

        self.state = 0

    def processMessage(self,msg):
        sender, value = msg
        debugPrint("AndGate received %s from %s" % (value,sender))

        if sender is self.inputA:
            self.inputAstate = value
        elif sender is self.inputB:
            self.inputBstate = value
        else:
            raise RuntimeError("Didn't expect message from %s" % sender)

        if self.inputAstate and self.inputBstate:
            self.state = 1
        else:
            self.state = 0
        debugPrint("AndGate's new state => %s" % self.state)

    def notify(self,output):
        output((self,self.state))

在与门的 processMessage 方法中,我们需要判定,是哪个输入端送来了消息,并据此设置状态。这就是为什么别的部件送来的消息中需要含有其自身的引用。

最后我们做出或门。它和与门类似,只是,它只要有任一个输入端为1的时候就送出消息1,只有两个输入端都为0的时候才送出0:

class OrGate(EventHandler):
    def __init__(self,inputA,inputB,*outputs):
        EventHandler.__init__(self,*outputs)

        self.inputA = inputA
        self.inputAstate = inputA.state
        inputA.registerOutput(self)

        self.inputB = inputB
        self.inputBstate = inputB.state
        inputB.registerOutput(self)

        self.state = 0

    def processMessage(self,msg):
        sender, value = msg
        debugPrint("OrGate received %s from %s" % (value,sender))

        if sender is self.inputA:
            self.inputAstate = value
        elif sender is self.inputB:
            self.inputBstate = value
        else:
            raise RuntimeError("Didn't expect message from %s" % sender)

        if self.inputAstate or self.inputBstate:
            self.state = 1
        else:
            self.state = 0
        debugPrint("OrGate's new state => %s" % self.state)

    def notify(self,output):
        output((self,self.state))

5.6.1 半加器

作为结束,我们将使用我们已经建立的所有部件,来构建一个半加器。半加器实现两个比特的加法。我们将一些部件连接了起来,然后来“拨动”开关。开关的动作改变了其状态,并且把其变化,以数据流的方式,通过系统传播了下去:

if __name__ == "__main__":
    # 半加器
    inputA = Switch()
    inputB = Switch()
    result = Reporter("Result = %(value)s")
    carry = Reporter("Carry = %(value)s")
    andGateA = AndGate(inputA,inputB,carry)
    orGate = OrGate(inputA,inputB)
    inverter = Inverter(andGateA)
    andGateB = AndGate(orGate,inverter,result)
    inputA(1)
    inputB(1)
    inputB(0)
    inputA(0)

6 角色

在角色的模型里面,一切都是角色(废话!)。角色就是一个对象(一般意义上的对象,而不必是面向对象中的意义),它可以:

  • 从其他角色接收消息。
  • 对收到的消息中适合于自己的,进行处理。
  • 向其它角色发送消息。
  • 创建新的角色。

一个角色对其它的角色并不具有直接的访问渠道,所有的交流都通过消息传递来完成。这就提供了丰富的模型,来模拟现实世界中的对象——它们是彼此松散耦合的,并对彼此的内部所知有限。

如果我们要建立一个模拟过程的话,就来模拟一下……

6.1 杀手机器人

Note

本节的完整程序保存为actors.py,在本文的末尾,和代码zip文件中都有。

6.1.1 角色基类

在这个例子中,我们将配置出一个小小的世界,在其中有一些使用角色模型来移动和战斗的机器人。作为开始,我们来定义所有角色的基类:

class actor:
    def __init__(self):
        self.channel = stackless.channel()
        self.processMessageMethod = self.defaultMessageAction
        stackless.tasklet(self.processMessage)()

    def processMessage(self):
        while 1:
            self.processMessageMethod(self.channel.receive())

    def defaultMessageAction(self,args):
        print args

默认情况下,角色建立一个通道来接收消息,指定一个方法来处理这些消息,并启动一个循环来将接收的消息分派给处理方法。默认的处理过程只是把收到的消息显示出来。这些,已经是我们实现角色模型所需要的全部。

6.1.2 消息的格式

所有发送的消息都遵从一个格式:先是发送者的通道,接着一个字符串为消息的名称,再接下来是可选的参数。例如:

(self.channel, "JOIN", (1,1) )
(self.channel, "COLLISION")
等等……

注意,我们只将发送者的通道随消息送出,而不是整个发送者对象。在角色模型中,角色间的所有交流都必须通过消息传递来体现,如果将 self 都发送出去的话,则使得对方可以很容易地用非正常手段对发送者的内部未知信息进行访问。

事实上你会注意到,当我们将本节的大部分角色实例化的时候,甚至不需要将其赋值给能被别的角色访问到的变量。我们仅仅创建它们,并让它们独自漂浮在那里,对周围环境只有有限的了解。

6.1.3 世界(world)类

“世界”角色,扮演着其他所有角色相互作用的中央枢纽。其他角色发送 JOIN 消息给世界角色,后者则跟踪它们。周期性地,世界角色发出 WORLD_STATE 消息,其中包括关于所有可见的角色的信息,来供它们内部处理:

class world(actor):
    def __init__(self):
        actor.__init__(self)
        self.registeredActors = {}
        stackless.tasklet(self.sendStateToActors)()

    def testForCollision(self,x,y):
        if x < 0 or x > 496:
            return 1
        elif y < 0 or y > 496:
            return 1
        else:
            return 0

    def sendStateToActors(self):
        while 1:
            for actor in self.registeredActors.keys():
                actorInfo = self.registeredActors[actor]
                if self.registeredActors[actor][1] != (-1,-1):
                    VectorX,VectorY = (math.sin(math.radians(actorInfo[2])) * actorInfo[3],
                                       math.cos(math.radians(actorInfo[2])) * actorInfo[3])
                    x,y = actorInfo[1]
                    x += VectorX
                    y -= VectorY
                    if self.testForCollision(x,y):
                        actor.send((self.channel,"COLLISION"))
                    else:
                        self.registeredActors[actor] = tuple([actorInfo[0],
                                                          (x,y),
                                                              actorInfo[2],
                                                              actorInfo[3]])
            worldState = [self.channel, "WORLD_STATE"]
            for actor in self.registeredActors.keys():
                if self.registeredActors[actor][1] != (-1,-1):
                    worldState.append( (actor, self.registeredActors[actor]))
            message = tuple(worldState)
            for actor in self.registeredActors.keys():
                actor.send(message)
            stackless.schedule()

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "JOIN":
            print 'ADDING ' , msgArgs
            self.registeredActors[sentFrom] = msgArgs
        elif msg == "UPDATE_VECTOR":
            self.registeredActors[sentFrom] = tuple([self.registeredActors[sentFrom][0],
                                                     self.registeredActors[sentFrom][1],
                                                     msgArgs[0],msgArgs[1]])
        else:
            print '!!!! WORLD GOT UNKNOWN MESSAGE ' , args

World = world().channel

除了处理消息的微进程外,“世界”角色还建立了另一个独立的微进程,来执行 sendStateToActors() 方法。这个方法里有个循环,用于构建关于世界状态的信息,并发送给所有的角色。这是其它角色唯一可以指望接收到的消息。若有必要,它们可以回应这个消息, 即将某种 UPDATE 消息发回给世界。

作为 sendStateToActors() 方法的一部分,“世界”角色需要更新其内部的、对可移动的角色的位置的记录。它使用可移动角色的角度和速度来建立一个矢量,确保更新后的位置不会撞到世界的四面边界,并存下其新的位置。

defaultMessageAction() 方法处理以下已知信息,并忽略其他的:

JOIN
将一个角色添加到世界中的已知角色列表,其参数包括新角色的位置、角度和速度。位置-1, -1表示这个角色对其它角色不可见,比如后面将要详述的显示屏角色。
UPDATE_VECTOR
为发送这个消息的角色设置新的角度和速度。

最后,一个“世界”角色被实例化,其通道被保存进全局变量 World 中,使其它角色可以发送它们最初的 JOIN 消息。

6.1.4 一个简单机器人

我们将以一个简单的机器人开始,它以恒定的速度移动,每接到一个 WORLD_STATE 消息的时候,都顺时针旋转1度作为响应。当发生与世界边界碰撞的 COLLISION 事件时,它将旋转73度再尝试前进。所有其他消息将被忽略。

class basicRobot(actor):
    def __init__(self,location=(0,0),angle=135,velocity=1,world=World):
        actor.__init__(self)
        self.location = location
        self.angle = angle
        self.velocity = velocity
        self.world = world

        joinMsg =(self.channel,"JOIN",self.__class__.__name__,
                  self.location,self.angle,self.velocity)
        self.world.send(joinMsg)

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            self.location = (self.location[0] + 1, self.location[1] + 1)
            self.angle += 1
            if self.angle >= 360:
                self.angle -= 360

            updateMsg = (self.channel, "UPDATE_VECTOR",
                         self.angle,self.velocity)
            self.world.send(updateMsg)
        elif msg == "COLLISION":
            self.angle += 73
            if self.angle >= 360:
                self.angle -= 360
        else:
            print "UNKNOWN MESSAGE", args

basicRobot(angle=135,velocity=5)
basicRobot((464,0),angle=225,velocity=10)

stackless.run()

注意,机器人的构造方法发出了一个 JOIN 消息到“世界”对象,来注册自己。除此之外,代码应该还算易懂的。

6.1.5 蹊跷(Detour)的PyGame

至此,在示例程序中,我们都是使用调试输出语句来显示事情进行的过程。我试图以这种方式来保持代码的简单易懂,但有些时候,输出语句却变得不再直观,而是越发迷惑。在“数据流”一节中我们已经用得很勉强了,而在本节中,情况已经变得复杂到无法再尝试用打印输出来表示了。

Note

要运行本节的代码,需要安装 pyGame 的当前版本,可以从这里取得:http://www.pygame.org/

我决定使用 pyGame 来创建一个简单的可视化引擎。尽管对于 pyGame 内容的叙述已经超出了本教程的范围,但其操作本身还是相对简明的。当显示屏角色收到一个 WORLD_STATE 消息,就将相应的角色放置上去,并更新显示。很幸运,我们可以将所有的 pyGame 代码隔离在一个角色之内,因此代码的其它部分可以保持不被“污染”,依然可以被理解,哪怕不了解也不关心 pyGame 怎么进行的显示渲染:

class display(actor):
    def __init__(self,world=World):
        actor.__init__(self)

        self.world = World

        self.icons = {}
        pygame.init()

        window = pygame.display.set_mode((496,496))
        pygame.display.set_caption("Actor Demo")

        joinMsg = (self.channel,"JOIN",self.__class__.__name__, (-1,-1))
        self.world.send(joinMsg)

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            self.updateDisplay(msgArgs)
        else:
            print "UNKNOWN MESSAGE", args

    def getIcon(self, iconName):
        if self.icons.has_key(iconName):
            return self.icons[iconName]
        else:
            iconFile = os.path.join("data","%s.bmp" % iconName)
            surface = pygame.image.load(iconFile)
            surface.set_colorkey((0xf3,0x0a,0x0a))
            self.icons[iconName] = surface
            return surface

    def updateDisplay(self,actors):

        for event in pygame.event.get():
            if event.type == pygame.QUIT: sys.exit()

        screen = pygame.display.get_surface()

        background = pygame.Surface(screen.get_size())
        background = background.convert()
        background.fill((200, 200, 200))

        screen.blit(background, (0,0))
        for item in actors:
            screen.blit(pygame.transform.rotate(self.getIcon(item[1][0]),-item[1][2]), item[1][1])
        pygame.display.flip()

display()

这里获取了 WORLD_STATE ,并基于此创建了显示屏。

Note

为了使本节的示例得以运行,需要在Python下安装 pyGame 。你也可能需要下载我所建立的、可选的图标,并将其解压到你的代码目录中。

6.1.6 第一轮代码

现在我们做了足够的准备,来运行程序的第一个版本。运行后,两个简单机器人将会开始迂回移动,并在边界反弹。

6.2 又一蹊跷:机理的模拟

Note

本节的完整程序保存为 actors2.py ,在本文的末尾,和代码.zip文件中和都有。

作为另一条蹊径,我们需要实现某种游戏(呃……我是指,模拟)的机理。严格来说,这些机理与角色模型一点关系都没有。然而,为了建立一种丰富而逼真 的模拟,我们不能被它们所阻碍。本节将详述我们将要达到的目标,以及如何达到。在这之后,我们用来摆弄角色的这个环境将变得更加有用。

6.2.1 角色属性

随着“世界”角色需要跟踪的信息变得越发复杂,在初始的 JOIN 消息中带送一串参数的方式也变得越来越麻烦。为了使这更容易,我们建立一个属性集对象,来跟踪这些信息,这个将取代分立的参数,在 JOIN 消息中被传递。

class properties:
    def __init__(self,name,location=(-1,-1),angle=0,
                 velocity=0,height=-1,width=-1,hitpoints=1,physical=True,
                 public=True):
        self.name = name
        self.location = location
        self.angle = angle
        self.velocity = velocity
        self.height = height
        self.width = width
        self.public = public
        self.hitpoints = hitpoints

注意,属性集对象是为了在角色之间传递信息而建立的,我们不会在建立它的那个角色局部再存储一份,否则,“世界”角色就能够直接修改其它角色的内部内容了,而不是通过发送消息来修改。

6.2.2 碰撞检测

在上一个程序中,碰撞检测的过程是有点问题的,其中最明显的就是角色不会彼此相撞,两个弹来跳去的机器人只会彼此穿过,而不是发生碰撞。第二个问题 则是我们没有指明角色的尺寸,这在机器人撞到右边界或下边界的时候表现最为明显:在 COLLISION 消息起作用之前,它们看上去有部分越过了边界。我确信,碰撞检测这主题是可以写几本书的,但这里我们将试图坚持使用一个相当简单的版本,对我们的目的来说 这已经够好了。

首先,我们给每个角色加上高度与宽度属性,这就能够给角色建立一个“边框”。原来的“位置”属性已经确定了这个边框的左上角,而新加的高度和宽度属性则同时确定了其右下角,这就对角色的物理形状给出了一个合理的近似表示。

为了检测与世界边缘的碰撞,现在,我们检测角色边框的四角中任一个是否与边缘有碰撞;为了检测和其它物体的碰撞,我们将维护一个已被检测过碰撞的物体的列表。我们遍历这个列表,找出是否有哪个角色的四角之一处在了另一个角色的边框之内,若存在,则它们是碰撞的。

这些就是我们的基本碰撞检测系统的全部,下面是检测单个碰撞的函数:

def testForCollision(self,x,y,item,otherItems=[]):
    if x < 0 or x + item.width > 496:
        return self.channel
    elif y < 0 or y+ item.height > 496:
        return self.channel
    else:
        ax1,ax2,ay1,ay2 = x, x+item.width, y,y+item.height
        for item,bx1,bx2,by1,by2 in otherItems:
            if self.registeredActors[item].physical == False: continue
            for x,y in [(ax1,ay1),(ax1,ay2),(ax2,ay1),(ax2,ay2)]:
                if x >= bx1 and x <= bx2 and y >= by1 and y <= by2:
                    return item
            for x,y in [(bx1,by1),(bx1,by2),(bx2,by1),(bx2,by2)]:
                if x >= ax1 and x <= ax2 and y >= ay1 and y <= ay2:
                    return item
        return None

还有个方法,用来遍历所有角色并检测,它在 sendStateToActors() 微进程中被调用:

def updateActorPositions(self):
    actorPositions = []
    for actor in self.registeredActors.keys():
        actorInfo = self.registeredActors[actor]
        if actorInfo.public and actorInfo.physical:
            x,y = actorInfo.location
            angle = actorInfo.angle
            velocity = actorInfo.velocity
            VectorX,VectorY = (math.sin(math.radians(angle)) * velocity,
                               math.cos(math.radians(angle)) * velocity)
            x += VectorX/self.updateRate
            y -= VectorY/self.updateRate
            collision = self.testForCollision(x,y,actorInfo,actorPositions)
            if collision:
                #不移动
                actor.send((self.channel,"COLLISION",actor,collision))
                if collision and collision is not self.channel:
                    collision.send((self.channel,"COLLISION",actor,collision))
            else:
                actorInfo.location = (x,y)
            actorPositions.append( (actor,
                                    actorInfo.location[0],
                                    actorInfo.location[0] + actorInfo.height,
                                    actorInfo.location[1],
                                    actorInfo.location[1] + actorInfo.width))

6.2.3 恒定的时间

我们的模拟有另一个问题,就是它在不同的机器上运行,会消耗不同的时间。若你的机器比我的快,你可能基本看不清机器人了,或者比我的慢,则机器人好像在爬一样。

为了修正这个,我们将以恒定的速度来产生 WORLD_STATE 消息,默认情况下每1/30秒产生一次。如果这点可以标准化,事情就容易了。然而,在机器无法应付负载量的情况下,我们也要有能力进行修正,来维持这个刷 新率。如果产生一个帧的时间超过了1/30秒(不管是由于程序本身的复杂性,还是外部某个程序狂占资源),我们就需要调整刷新率。

在我们的例子中,如果完成一帧的计算的时间超过了刷新率所确定的周期时间,就将每秒刷新次数降低一次;在在当前刷新率下,若显示一帧的过程中的空闲时间占到了40%或更多,则将每秒刷新次数增加一次,最大不超过每秒30次刷新。

这样,我们就能够在不同的机器上以同样的速度运行了,但这引入了另一个有趣的问题:例如现在,在每次刷新的时候,我们都将机器人的角度调整1度,并 重设其速度。这样的话,我们让这程序在不同的机器上各运行10秒钟,其中一台机器跑到每秒20次刷新,另一台则跑到每秒30次,那么最后,两台机器上的机 器人的位置将不会相同,这显然是不应该的。我们需要将角色数据的更新也改成基于时间的。

在本例中,我们不再使用每帧调整1度角度并调整一次速度(例如5点)的做法,而是将这个调整变成基于流逝时间的,比如,可以在每个时间单位里调整角度30.0度,调整速度150.0点。这样就可以在任何刷新率下都得到一致的行为。

为了实现这点,我们将修改 WORLD_STATE 消息,使之既包含当前时间又包含刷新率。于是,收到这种消息的角色就可以计算出合适的更新结果。

实现刷新率的代码如下:

def runFrame(self):
    initialStartTime = time.clock()
    startTime = time.clock()
    while 1:
        self.killDeadActors()
        self.updateActorPositions()
        self.sendStateToActors(startTime)
        #等待
        calculatedEndTime = startTime + 1.0/self.updateRate

        doneProcessingTime = time.clock()
        percentUtilized =  (doneProcessingTime - startTime) / (1.0/self.updateRate)
        if percentUtilized >= 1:
            self.updateRate -= 1
            print "TOO MUCH LOWERING FRAME RATE: " , self.updateRate
        elif percentUtilized <= 0.6 and self.updateRate < self.maxupdateRate:
            self.updateRate += 1
            print "TOO MUCH FREETIME, RAISING FRAME RATE: " , self.updateRate

        while time.clock() < calculatedEndTime:
            stackless.schedule()
        startTime = calculatedEndTime

        stackless.schedule()

6.2.4 伤害值、生命值和死亡

现在,我们的机器人是无敌的,将会永远运行下去,这可不怎么好玩——它们应该只能承受有限的伤害,然后死亡才对。为了实现这个,我们得增加一些新型 的消息:DAMAGE 消息包含一个参数,指定其受到的伤害值,这个值将从机器人类的另一个新属性“生命值”(hitpoints)之中减去。如果生命值达到小于或等于0,则这 个角色将向“世界”角色发送一条 KILLME 消息。以下是从机器人的 defaultMessageAction() 方法中摘录的的代码,用来实现这一过程:

elif msg == "DAMAGE":
    self.hitpoints -= msgArgs[0]
    if self.hitpoints <= 0:
        self.world.send( (self.channel,"KILLME") )
else:
    print "UNKNOWN MESSAGE", args

另外,我们随手规定, COLLISION 消息也会使生命值降低一点,如果满足条件,也会导致发送 KILLME 消息。

当世界角色收到 KILLME 消息,便将其内部对这个角色的生命值的记录修改为0,接着,将删除那些生命值小于或等于0的角色,作为常规更新的一部分:

def killDeadActors(self):
    for actor in self.registeredActors.keys():
        if self.registeredActors[actor].hitpoints <= 0:
            print "ACTOR DIED", self.registeredActors[actor].hitpoints
            actor.send_exception(TaskletExit)
            del self.registeredActors[actor]

注意,在这里我们引入了通道的 send_exception() 方法。与普通的 send 不同,这将使接收的微进程所调用的 channel.receive() 方法抛出一个异常。此处我们抛出的是 stackless 中的 TaskletExit 异常,这将使一个微进程安静地结束。你也可以抛出其他任何异常,但如果不加以捕获的话,这个异常就会在主微进程中再被抛出一次。

6.2.5 第二轮代码

这个程序的完整版本,仍然算不上特别令人激动,但如果你运行它,你将看到,上面我们所添加的所有特性都起了作用:机器人在经过了足够数量的碰撞之后,最终会死亡,从屏幕上消失。

6.3 回到角色:一起抓狂

Note

本节的完整程序保存为 actors3.py ,在本文的末尾,和代码.zip文件中和都有。

现在对于机理的模拟已经不成问题,我们就可以开始用这个程序做一些有趣的事情了,首先……

6.3.1 爆炸

机器人死亡的时候,让它们简单地消失可没什么意思,至少应该让它们爆炸。机器人在死亡的时候将会创建一个“爆炸”角色,这个角色并不是物质化的,而是仅仅显示一个爆炸的图像。它会在3秒钟后将自己删除,爆炸图像也随之消失:

class explosion(actor):
    def __init__(self,location=(0,0),angle=0,world=World):
        actor.__init__(self)
        self.time = 0.0
        self.world = world
        self.world.send((self.channel,"JOIN",
                            properties(self.__class__.__name__,
                                       location = location,
                                       angle = angle,
                                       velocity=0,
                                       height=32.0,width=32.0,hitpoints=1,
                                       physical=False)))

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            WorldState = msgArgs[0]
            if self.time == 0.0:
                self.time = WorldState.time
            elif WorldState.time >= self.time + 3.0:
                self.world.send( (self.channel, "KILLME") )

6.3.2 埋雷机器人

现在我们来创建能埋地雷的机器人。在创建这种机器人的类之前,先得创建一个“地雷”类:

class mine(actor):
    def __init__(self,location=(0,0),world=World):
        actor.__init__(self)
        self.world = world
        self.world.send((self.channel,"JOIN",
                            properties(self.__class__.__name__,
                                       location=location,
                                       angle=0,
                                       velocity=0,
                                       height=2.0,width=2.0,hitpoints=1)))

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            pass
        elif msg == "COLLISION":
            if msgArgs[0] is self.channel:
                other = msgArgs[1]
            else:
                other = msgArgs[0]
            other.send( (self.channel,"DAMAGE",25) )
            self.world.send( (self.channel,"KILLME"))
            print "MINE COLLISION"
        else:
            print "UNKNOWN MESSAGE", args

这是个简单的角色,只是单纯地停在那里,直到有东西撞上它,就向这个撞它的东西送出25点伤害,并将自己删除。

埋雷机器人与普通机器人相似,除了几点不同:首先,为了使局面混杂,我将埋雷机器人设定为为曲折移动,而不是缓慢地转向一个方向。其次,它每一秒都创建一个地雷,立即放置在自己身后:

class minedropperRobot(actor):
    def __init__(self,location=(0,0),angle=135,velocity=1,
                 hitpoints=20,world=World):
        actor.__init__(self)
        self.location = location
        self.angle = angle
        self.delta = 0.0
        self.height=32.0
        self.width=32.0
        self.deltaDirection = "up"
        self.nextMine = 0.0
        self.velocity = velocity
        self.hitpoints = hitpoints
        self.world = world
        self.world.send((self.channel,"JOIN",
                            properties(self.__class__.__name__,
                                       location=self.location,
                                       angle=self.angle,
                                       velocity=self.velocity,
                                       height=self.height,width=self.width,
                                       hitpoints=self.hitpoints)))

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            for actor in msgArgs[0].actors:
                if actor[0] is self.channel:
                    break
            self.location = actor[1].location
            if self.deltaDirection == "up":
                self.delta += 60.0 * (1.0 / msgArgs[0].updateRate)
                if self.delta > 15.0:
                    self.delta = 15.0
                    self.deltaDirection = "down"
            else:
                self.delta -= 60.0 * (1.0 / msgArgs[0].updateRate)
                if self.delta < -15.0:
                    self.delta = -15.0
                    self.deltaDirection = "up"
            if self.nextMine <= msgArgs[0].time:
                self.nextMine = msgArgs[0].time + 1.0
                mineX,mineY = (self.location[0] + (self.width / 2.0) ,
                               self.location[1] + (self.width / 2.0))

                mineDistance = (self.width / 2.0 ) ** 2
                mineDistance += (self.height / 2.0) ** 2
                mineDistance = math.sqrt(mineDistance)

                VectorX,VectorY = (math.sin(math.radians(self.angle + self.delta)),
                                   math.cos(math.radians(self.angle + self.delta)))
                VectorX,VectorY = VectorX * mineDistance,VectorY * mineDistance
                x,y = self.location
                x += self.width / 2.0
                y += self.height / 2.0
                x -= VectorX
                y += VectorY
                mine( (x,y))

            updateMsg = (self.channel, "UPDATE_VECTOR",
                         self.angle + self.delta ,self.velocity)
            self.world.send(updateMsg)
        elif msg == "COLLISION":
            self.angle += 73.0
            if self.angle >= 360:
                self.angle -= 360
            self.hitpoints -= 1
            if self.hitpoints <= 0:
                explosion(self.location,self.angle)
                self.world.send((self.channel,"KILLME"))
        elif msg == "DAMAGE":
            self.hitpoints -= msgArgs[0]
            if self.hitpoints <= 0:
                explosion(self.location,self.angle)
                self.world.send((self.channel, "KILLME"))
        else:
            print "UNKNOWN MESSAGE", args

6.3.3 建造台

建造台每5秒钟就在其位置上简单地创建一个新的、具有随机的属性的机器人。在其构造函数中有一些猫腻:它并不是特意创建一个含有所有合法的机器人的 数组,而是使用自省,来查找所有的名称以“Robot”结尾的类,并将它们加入到一个列表。这样,如果你创建一个自己的机器人类,你将不需要在这个建造台 类上特意进行某种注册工作。这个类除此之外的部分还是相当易懂的:

class spawner(actor):
    def __init__(self,location=(0,0),world=World):
        actor.__init__(self)
        self.location = location
        self.time = 0.0
        self.world = world

        self.robots = []
        for name,klass in globals().iteritems():
            if name.endswith("Robot"):
                self.robots.append(klass)

        self.world.send((self.channel,"JOIN",
                            properties(self.__class__.__name__,
                                       location = location,
                                       angle=0,
                                       velocity=0,
                                       height=32.0,width=32.0,hitpoints=1,
                                       physical=False)))

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            WorldState = msgArgs[0]
            if self.time == 0.0:
                self.time = WorldState.time + 0.5 # 启动时等待1/2秒
            elif WorldState.time >= self.time: # 每5秒
                self.time = WorldState.time + 5.0
            angle = random.random() * 360.0
            velocity = random.random() * 1000.0
            newRobot = random.choice(self.robots)
            newRobot(self.location,angle,velocity)

6.3.4 最终的模拟

最后,我们在世界的四角和中央各放置一个建造台。现在我们的模拟系统能够一直运行,并不断造出新的机器人。我们可以添加新的机器人种类,来随便摆弄这个系统。

6.4 总结

我们已经设法用少量的代码,建立了一个相当复杂的模拟系统,更重要的是,甚至每个角色都在独立运行。如果你将我们已经在传递的各个消息看作一个API,那它还真不是很复杂:

  • WORLD_STATE
  • JOIN
  • UPDATE_VECTOR
  • COLLISION
  • KILLME
  • DAMAGE

除此以外,一个角色所必需的其他所有内容,都被封装在其内部。为了了解外部的世界,它只需要处理这六条消息,这种机制不仅简化了程序本身,也简化了我们对其的理解过程。

7 完整代码列表

7.1 pingpong.py递归的乒乓球示例

def ping():
    print "PING"
    pong()

def pong():
    print "PONG"
    ping()

ping()

7.2 pingpong_stackless.py无堆栈的乒乓球示例

#
# pingpong_stackless.py
#

import stackless

ping_channel = stackless.channel()
pong_channel = stackless.channel()

def ping():
    while ping_channel.receive(): #blocks here
        print "PING"
        pong_channel.send("from ping")

def pong():
    while pong_channel.receive():
        print "PONG"
        ping_channel.send("from pong")

stackless.tasklet(ping)()
stackless.tasklet(pong)()

# we need to 'prime' the game by sending a start message
# if not, both tasklets will block
stackless.tasklet(ping_channel.send)('startup')

stackless.run()

7.3 hackysackthreaded.py基于操作系统线程的hackysack示例

import thread
import random
import sys
import Queue

class hackysacker:
    counter = 0
    def __init__(self,name,circle):
        self.name = name
        self.circle = circle
        circle.append(self)
        self.messageQueue = Queue.Queue()

        thread.start_new_thread(self.messageLoop,())

    def incrementCounter(self):
        hackysacker.counter += 1
        if hackysacker.counter >= turns:
            while self.circle:
                hs = self.circle.pop()
                if hs is not self:
                    hs.messageQueue.put('exit')
            sys.exit()

    def messageLoop(self):
        while 1:
            message = self.messageQueue.get()
            if message == "exit":
                debugPrint("%s is going home" % self.name)
                sys.exit()
            debugPrint("%s got hackeysack from %s" % (self.name, message.name))
            kickTo = self.circle[random.randint(0,len(self.circle)-1)]
            debugPrint("%s kicking hackeysack to %s" % (self.name, kickTo.name))
            self.incrementCounter()
            kickTo.messageQueue.put(self)

def debugPrint(x):
    if debug:
        print x

debug=1
hackysackers=5
turns = 5

def runit(hs=10,ts=10,dbg=1):
    global hackysackers,turns,debug
    hackysackers = hs
    turns = ts
    debug = dbg

    hackysacker.counter= 0
    circle = []
    one = hackysacker('1',circle)

    for i in range(hackysackers):
        hackysacker(`i`,circle)

    one.messageQueue.put(one)

    try:
        while circle:
            pass
    except:
        #sometimes we get a phantom error on cleanup.
        pass

if __name__ == "__main__":
    runit(dbg=1)

7.4 hackysackstackless.py stackless的hackysack示例

import stackless
import random
import sys

class hackysacker:
    counter = 0
    def __init__(self,name,circle):
        self.name = name
        self.circle = circle
        circle.append(self)
        self.channel = stackless.channel()

        stackless.tasklet(self.messageLoop)()

    def incrementCounter(self):
        hackysacker.counter += 1
        if hackysacker.counter >= turns:
            while self.circle:
                self.circle.pop().channel.send('exit')

    def messageLoop(self):
        while 1:
            message = self.channel.receive()
            if message == 'exit':
                return
            debugPrint("%s got hackeysack from %s" % (self.name, message.name))
            kickTo = self.circle[random.randint(0,len(self.circle)-1)]
            while kickTo is self:
                kickTo = self.circle[random.randint(0,len(self.circle)-1)]
            debugPrint("%s kicking hackeysack to %s" % (self.name, kickTo.name))
            self.incrementCounter()
            kickTo.channel.send(self)

def debugPrint(x):
    if debug:print x

debug = 5
hackysackers = 5
turns = 1

def runit(hs=5,ts=5,dbg=1):
    global hackysackers,turns,debug
    hackysackers = hs
    turns = ts
    debug = dbg

    hackysacker.counter = 0
    circle = []
    one = hackysacker('1',circle)

    for i in range(hackysackers):
        hackysacker(`i`,circle)

    one.channel.send(one)

    try:
        stackless.run()
    except TaskletExit:
        pass

if __name__ == "__main__":
    runit()

7.5 assemblyline.py普通的生产线示例

class storeroom:
    def __init__(self,name,product,unit,count):
        self.product = product
        self.unit = unit
        self.count = count
        self.name = name

    def get(self,count):
        if count > self.count:
            raise RuntimeError("Not enough %s" % self.product)
        else:
            self.count -= count
        return count

    def put(self,count):
        self.count += count

    def run(self):
        pass

rivetStoreroom = storeroom("rivetStoreroom","rivets","#",1000)
plasticStoreroom = storeroom("plastic Storeroom","plastic pellets","lb",100)

class injectionMolder:
    def __init__(self,name,partName,plasticSource,plasticPerPart,timeToMold):
        self.partName = partName
        self.plasticSource = plasticSource
        self.plasticPerPart = plasticPerPart
        self.timeToMold = timeToMold
        self.items = 0
        self.plastic = 0
        self.time = -1
        self.name = name

    def get(self,items):
        if items > self.items:
            return 0
        else:
            self.items -= items
            return items

    def run(self):
        if self.time == 0:
            self.items += 1
            print "%s finished making part" % self.name
            self.time -= 1
        elif self.time < 0:
            print "%s starts making new part %s" % (self.name,self.partName)
            if self.plastic < self.plasticPerPart:
                print "%s getting more plastic"
                self.plastic += self.plasticSource.get(self.plasticPerPart * 10)
            self.time = self.timeToMold
        else:
            print "%s molding for %s more seconds" % (self.partName, self.time)
            self.time -= 1

armMolder = injectionMolder("arm Molder", "arms",plasticStoreroom,0.2,6)
legMolder = injectionMolder("leg Molder", "leg",plasticStoreroom,0.2,5)
headMolder = injectionMolder("head Molder","head",plasticStoreroom,0.1,4)
torsoMolder = injectionMolder("torso Molder","torso",plasticStoreroom,0.5,10)

class assembler:
    def __init__(self,name,partAsource,partBsource,rivetSource,timeToAssemble):
        self.partAsource = partAsource
        self.partBsource = partBsource
        self.rivetSource = rivetSource
        self.timeToAssemble = timeToAssemble
        self.itemA = 0
        self.itemB = 0
        self.items = 0
        self.rivets = 0
        self.time = -1
        self.name = name

    def get(self,items):
        if items > self.items:
            return 0
        else:
            self.items -= items
            return items

    def run(self):
        if self.time == 0:
            self.items += 1
            print "%s finished assembling part" % self.name
            self.time -= 1
        elif self.time < 0:
            print "%s starts assembling new part" % self.name
            if self.itemA < 1:
                print "%s Getting item A" % self.name
                self.itemA += self.partAsource.get(1)
                if self.itemA < 1:
                    print "%s waiting for item A" % self.name
            elif self.itemB < 1:
                print "%s Getting item B" % self.name
                self.itemB += self.partBsource.get(1)
                if self.itemB < 1:
                    print "%s waiting for item B" % self.name
            print "%s starting to assemble" % self.name
            self.time = self.timeToAssemble
        else:
            print "%s assembling for %s more seconds" % (self.name, self.time)
            self.time -= 1

legAssembler = assembler("leg Assembler",torsoMolder,legMolder,rivetStoreroom,2)
armAssembler = assembler("arm Assembler", armMolder,legAssembler,rivetStoreroom,2)
torsoAssembler = assembler("torso Assembler", headMolder,armAssembler,
                        rivetStoreroom,3)

components = [rivetStoreroom, plasticStoreroom, armMolder,
              legMolder, headMolder, torsoMolder,
              legAssembler, armAssembler, torsoAssembler]

def run():
    while 1:
        for component in components:
            component.run()
        raw_input("Press <ENTER> to continue...")
        print "nnn"

if __name__ == "__main__":
    run()

7.6 assemblyline-stackless.py stackless的生产线示例

class storeroom:
    def __init__(self,name,product,unit,count):
        self.product = product
        self.unit = unit
        self.count = count
        self.name = name

    def get(self,count):
        if count > self.count:
            raise RuntimeError("Not enough %s" % self.product)
        else:
            self.count -= count
        return count

    def put(self,count):
        self.count += count

    def run(self):
        pass

rivetStoreroom = storeroom("rivetStoreroom","rivets","#",1000)
plasticStoreroom = storeroom("plastic Storeroom","plastic pellets","lb",100)

class injectionMolder:
    def __init__(self,name,partName,plasticSource,plasticPerPart,timeToMold):
        self.partName = partName
        self.plasticSource = plasticSource
        self.plasticPerPart = plasticPerPart
        self.timeToMold = timeToMold
        self.items = 0
        self.plastic = 0
        self.time = -1
        self.name = name

    def get(self,items):
        if items > self.items:
            return 0
        else:
            self.items -= items
            return items

    def run(self):
        if self.time == 0:
            self.items += 1
            print "%s finished making part" % self.name
            self.time -= 1
        elif self.time < 0:
            print "%s starts making new part %s" % (self.name,self.partName)
            if self.plastic < self.plasticPerPart:
                print "%s getting more plastic"
                self.plastic += self.plasticSource.get(self.plasticPerPart * 10)
            self.time = self.timeToMold
        else:
            print "%s molding for %s more seconds" % (self.partName, self.time)
    self.time -= 1

armMolder = injectionMolder("arm Molder", "arms",plasticStoreroom,0.2,6)
legMolder = injectionMolder("leg Molder", "leg",plasticStoreroom,0.2,5)
headMolder = injectionMolder("head Molder","head",plasticStoreroom,0.1,4)
torsoMolder = injectionMolder("torso Molder","torso",plasticStoreroom,0.5,10)

class assembler:
    def __init__(self,name,partAsource,partBsource,rivetSource,timeToAssemble):
        self.partAsource = partAsource
        self.partBsource = partBsource
        self.rivetSource = rivetSource
        self.timeToAssemble = timeToAssemble
        self.itemA = 0
        self.itemB = 0
        self.items = 0
        self.rivets = 0
        self.time = -1
        self.name = name

    def get(self,items):
        if items > self.items:
            return 0
        else:
            self.items -= items
            return items

    def run(self):
        if self.time == 0:
            self.items += 1
            print "%s finished assembling part" % self.name
            self.time -= 1
        elif self.time < 0:
            print "%s starts assembling new part" % self.name
            if self.itemA < 1:
                print "%s Getting item A" % self.name
                self.itemA += self.partAsource.get(1)
                if self.itemA < 1:
                    print "%s waiting for item A" % self.name
            elif self.itemB < 1:
                print "%s Getting item B" % self.name
                self.itemB += self.partBsource.get(1)
                if self.itemB < 1:
                    print "%s waiting for item B" % self.name
            print "%s starting to assemble" % self.name
            self.time = self.timeToAssemble
        else:
            print "%s assembling for %s more seconds" % (self.name, self.time)
    self.time -= 1

legAssembler = assembler("leg Assembler",torsoMolder,legMolder,rivetStoreroom,2)
armAssembler = assembler("arm Assembler", armMolder,legAssembler,rivetStoreroom,2)
torsoAssembler = assembler("torso Assembler", headMolder,armAssembler,
                            rivetStoreroom,3)

components = [rivetStoreroom, plasticStoreroom, armMolder,
          legMolder, headMolder, torsoMolder,
          legAssembler, armAssembler, torsoAssembler]

def run():
    while 1:
        for component in components:
            component.run()
        raw_input("Press <ENTER> to continue...")
        print "nnn"

if __name__ == "__main__":
    run()

7.7 digitalCircuit.py stackless的数字电路

import stackless

debug=0
def debugPrint(x):
    if debug:print x

class EventHandler:
    def __init__(self,*outputs):
        if outputs==None:
            self.outputs=[]
        else:
            self.outputs=list(outputs)

        self.channel = stackless.channel()
        stackless.tasklet(self.listen)()

    def listen(self):
        while 1:
            val = self.channel.receive()
            self.processMessage(val)
            for output in self.outputs:
                self.notify(output)

    def processMessage(self,val):
        pass

    def notify(self,output):
        pass

    def registerOutput(self,output):
        self.outputs.append(output)

    def __call__(self,val):
        self.channel.send(val)

class Switch(EventHandler):
    def __init__(self,initialState=0,*outputs):
        EventHandler.__init__(self,*outputs)
        self.state = initialState

    def processMessage(self,val):
        debugPrint("Setting input to %s" % val)
        self.state = val

    def notify(self,output):
        output((self,self.state))

class Reporter(EventHandler):
    def __init__(self,msg="%(sender)s send message %(value)s"):
        EventHandler.__init__(self)
        self.msg = msg

    def processMessage(self,msg):
        sender,value=msg
        print self.msg % {'sender':sender,'value':value}

class Inverter(EventHandler):
    def __init__(self,input,*outputs):
        EventHandler.__init__(self,*outputs)
        self.input = input
        input.registerOutput(self)
        self.state = 0

    def processMessage(self,msg):
        sender,value = msg
        debugPrint("Inverter received %s from %s" % (value,msg))
        if value:
            self.state = 0
        else:
            self.state = 1

    def notify(self,output):
        output((self,self.state))

class AndGate(EventHandler):
    def __init__(self,inputA,inputB,*outputs):
        EventHandler.__init__(self,*outputs)

        self.inputA = inputA
        self.inputAstate = inputA.state
        inputA.registerOutput(self)

        self.inputB = inputB
        self.inputBstate = inputB.state
        inputB.registerOutput(self)

        self.state = 0

    def processMessage(self,msg):
        sender, value = msg
        debugPrint("AndGate received %s from %s" % (value,sender))

        if sender is self.inputA:
            self.inputAstate = value
        elif sender is self.inputB:
            self.inputBstate = value
        else:
            raise RuntimeError("Didn't expect message from %s" % sender)

        if self.inputAstate and self.inputBstate:
            self.state = 1
        else:
            self.state = 0
        debugPrint("AndGate's new state => %s" % self.state)

    def notify(self,output):
        output((self,self.state))

class OrGate(EventHandler):
    def __init__(self,inputA,inputB,*outputs):
        EventHandler.__init__(self,*outputs)

        self.inputA = inputA
        self.inputAstate = inputA.state
        inputA.registerOutput(self)

        self.inputB = inputB
        self.inputBstate = inputB.state
        inputB.registerOutput(self)

        self.state = 0

    def processMessage(self,msg):
        sender, value = msg
        debugPrint("OrGate received %s from %s" % (value,sender))

        if sender is self.inputA:
            self.inputAstate = value
        elif sender is self.inputB:
            self.inputBstate = value
        else:
            raise RuntimeError("Didn't expect message from %s" % sender)

        if self.inputAstate or self.inputBstate:
            self.state = 1
        else:
            self.state = 0
        debugPrint("OrGate's new state => %s" % self.state)

    def notify(self,output):
        output((self,self.state))

if __name__ == "__main__":
    # half adder
    inputA = Switch()
    inputB = Switch()
    result = Reporter("Result = %(value)s")
    carry = Reporter("Carry = %(value)s")
    andGateA = AndGate(inputA,inputB,carry)
    orGate = OrGate(inputA,inputB)
    inverter = Inverter(andGateA)
    andGateB = AndGate(orGate,inverter,result)
    inputA(1)
    inputB(1)
    inputB(0)
    inputA(0)

7.8 actors.py第一个角色示例

import pygame
import pygame.locals
import os, sys
import stackless
import math

class actor:
    def __init__(self):
        self.channel = stackless.channel()
        self.processMessageMethod = self.defaultMessageAction
        stackless.tasklet(self.processMessage)()

    def processMessage(self):
        while 1:
            self.processMessageMethod(self.channel.receive())

    def defaultMessageAction(self,args):
        print args

class world(actor):
    def __init__(self):
        actor.__init__(self)
        self.registeredActors = {}
        stackless.tasklet(self.sendStateToActors)()

    def testForCollision(self,x,y):
        if x < 0 or x > 496:
            return 1
        elif y < 0 or y > 496:
            return 1
        else:
            return 0

    def sendStateToActors(self):
        while 1:
            for actor in self.registeredActors.keys():
                actorInfo = self.registeredActors[actor]
                if self.registeredActors[actor][1] != (-1,-1):
                    VectorX,VectorY = (math.sin(math.radians(actorInfo[2])) * actorInfo[3],
                                       math.cos(math.radians(actorInfo[2])) * actorInfo[3])
                    x,y = actorInfo[1]
                    x += VectorX
                    y -= VectorY
                    if self.testForCollision(x,y):
                        actor.send((self.channel,"COLLISION"))
                    else:
                        self.registeredActors[actor] = tuple([actorInfo[0],
                                                          (x,y),
                                                          actorInfo[2],
                                                          actorInfo[3]])
            worldState = [self.channel, "WORLD_STATE"]
            for actor in self.registeredActors.keys():
                if self.registeredActors[actor][1] != (-1,-1):
                    worldState.append( (actor, self.registeredActors[actor]))
            message = tuple(worldState)
            for actor in self.registeredActors.keys():
                actor.send(message)
            stackless.schedule()

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "JOIN":
            print 'ADDING ' , msgArgs
            self.registeredActors[sentFrom] = msgArgs
        elif msg == "UPDATE_VECTOR":
            self.registeredActors[sentFrom] = tuple([self.registeredActors[sentFrom][0],
                                                     self.registeredActors[sentFrom][1],
                                                     msgArgs[0],msgArgs[1]])
        else:
            print '!!!! WORLD GOT UNKNOWN MESSAGE ' , args

World = world().channel

class display(actor):
    def __init__(self,world=World):
        actor.__init__(self)

        self.world = World

        self.icons = {}
        pygame.init()

        window = pygame.display.set_mode((496,496))
        pygame.display.set_caption("Actor Demo")

        joinMsg = (self.channel,"JOIN",self.__class__.__name__, (-1,-1))
        self.world.send(joinMsg)

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            self.updateDisplay(msgArgs)
        else:
            print "UNKNOWN MESSAGE", args

    def getIcon(self, iconName):
        if self.icons.has_key(iconName):
            return self.icons[iconName]
        else:
            iconFile = os.path.join("data","%s.bmp" % iconName)
            surface = pygame.image.load(iconFile)
            surface.set_colorkey((0xf3,0x0a,0x0a))
            self.icons[iconName] = surface
            return surface

    def updateDisplay(self,actors):
        for event in pygame.event.get():
            if event.type == pygame.QUIT: sys.exit()

        screen = pygame.display.get_surface()

        background = pygame.Surface(screen.get_size())
        background = background.convert()
        background.fill((200, 200, 200))

        screen.blit(background, (0,0))
        for item in actors:
            screen.blit(pygame.transform.rotate(self.getIcon(item[1][0]),-item[1][2]), item[1][1])
        pygame.display.flip()

display()

class basicRobot(actor):
    def __init__(self,location=(0,0),angle=135,velocity=1,world=World):
        actor.__init__(self)
        self.location = location
        self.angle = angle
        self.velocity = velocity
        self.world = world

        joinMsg =(self.channel,"JOIN",self.__class__.__name__,
                  self.location,self.angle,self.velocity)
        self.world.send(joinMsg)

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            self.location = (self.location[0] + 1, self.location[1] + 1)
            self.angle += 1
            if self.angle >= 360:
                self.angle -= 360

            updateMsg = (self.channel, "UPDATE_VECTOR",
                         self.angle,self.velocity)
            self.world.send(updateMsg)
        elif msg == "COLLISION":
            self.angle += 73
            if self.angle >= 360:
                self.angle -= 360
        else:
            print "UNKNOWN MESSAGE", args

basicRobot(angle=135,velocity=5)
basicRobot((464,0),angle=225,velocity=10)

stackless.run()

7.9 actors2.py第二个角色示例

import pygame
import pygame.locals
import os, sys
import stackless
import math
import time

class actor:
    def __init__(self):
        self.channel = stackless.channel()
        self.processMessageMethod = self.defaultMessageAction
        stackless.tasklet(self.processMessage)()

    def processMessage(self):
        while 1:
            self.processMessageMethod(self.channel.receive())

    def defaultMessageAction(self,args):
        print args

class properties:
    def __init__(self,name,location=(-1,-1),angle=0,
                 velocity=0,height=-1,width=-1,hitpoints=1,physical=True,
                 public=True):
        self.name = name
        self.location = location
        self.angle = angle
        self.velocity = velocity
        self.height = height
        self.width = width
        self.public = public
        self.hitpoints = hitpoints
        self.physical = physical

class worldState:
    def __init__(self,updateRate,time):
        self.updateRate = updateRate
        self.time = time
        self.actors = []

class world(actor):
    def __init__(self):
        actor.__init__(self)
        self.registeredActors = {}
        self.updateRate = 30
        self.maxupdateRate = 30
        stackless.tasklet(self.runFrame)()

    def testForCollision(self,x,y,item,otherItems=[]):
        if x < 0 or x + item.width > 496:
            return self.channel
        elif y < 0 or y+ item.height > 496:
            return self.channel
        else:
            ax1,ax2,ay1,ay2 = x, x+item.width, y,y+item.height
            for item,bx1,bx2,by1,by2 in otherItems:
                if self.registeredActors[item].physical == False: continue
                for x,y in [(ax1,ay1),(ax1,ay2),(ax2,ay1),(ax2,ay2)]:
                    if x >= bx1 and x <= bx2 and y >= by1 and y <= by2:
                        return item
                for x,y in [(bx1,by1),(bx1,by2),(bx2,by1),(bx2,by2)]:
                    if x >= ax1 and x <= ax2 and y >= ay1 and y <= ay2:
                        return item
            return None

    def killDeadActors(self):
        for actor in self.registeredActors.keys():
            if self.registeredActors[actor].hitpoints <= 0:
                print "ACTOR DIED", self.registeredActors[actor].hitpoints
                actor.send_exception(TaskletExit)
                del self.registeredActors[actor]

    def updateActorPositions(self):
        actorPositions = []
        for actor in self.registeredActors.keys():
            actorInfo = self.registeredActors[actor]
            if actorInfo.public and actorInfo.physical:
                x,y = actorInfo.location
                angle = actorInfo.angle
                velocity = actorInfo.velocity
                VectorX,VectorY = (math.sin(math.radians(angle)) * velocity,
                                   math.cos(math.radians(angle)) * velocity)
                x += VectorX/self.updateRate
                y -= VectorY/self.updateRate
                collision = self.testForCollision(x,y,actorInfo,actorPositions)
                if collision:
                    #don't move
                    actor.send((self.channel,"COLLISION",actor,collision))
                    if collision and collision is not self.channel:
                        collision.send((self.channel,"COLLISION",actor,collision))
                else:
                    actorInfo.location = (x,y)
                actorPositions.append( (actor,
                                        actorInfo.location[0],
                                        actorInfo.location[0] + actorInfo.height,
                                        actorInfo.location[1],
                                        actorInfo.location[1] + actorInfo.width))

    def sendStateToActors(self,starttime):
        WorldState = worldState(self.updateRate,starttime)
        for actor in self.registeredActors.keys():
            if self.registeredActors[actor].public:
                WorldState.actors.append( (actor, self.registeredActors[actor]) )
        for actor in self.registeredActors.keys():
            actor.send( (self.channel,"WORLD_STATE",WorldState) )

    def runFrame(self):
        initialStartTime = time.clock()
        startTime = time.clock()
        while 1:
            self.killDeadActors()
            self.updateActorPositions()
            self.sendStateToActors(startTime)
            #wait
            calculatedEndTime = startTime + 1.0/self.updateRate

            doneProcessingTime = time.clock()
            percentUtilized =  (doneProcessingTime - startTime) / (1.0/self.updateRate)
            if percentUtilized >= 1:
                self.updateRate -= 1
                print "TOO MUCH LOWERING FRAME RATE: " , self.updateRate
            elif percentUtilized <= 0.6 and self.updateRate < self.maxupdateRate:
                self.updateRate += 1
                print "TOO MUCH FREETIME, RAISING FRAME RATE: " , self.updateRate

            while time.clock() < calculatedEndTime:
                stackless.schedule()
            startTime = calculatedEndTime

            stackless.schedule()

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "JOIN":
            print 'ADDING ' , msgArgs
            self.registeredActors[sentFrom] = msgArgs[0]
        elif msg == "UPDATE_VECTOR":
            self.registeredActors[sentFrom].angle = msgArgs[0]
            self.registeredActors[sentFrom].velocity = msgArgs[1]
        elif msg == "COLLISION":
            pass # known, but we don't do anything
        elif msg == "KILLME":
            self.registeredActors[sentFrom].hitpoints = 0
        else:
            print '!!!! WORLD GOT UNKNOWN MESSAGE ' , args

World = world().channel

class display(actor):
    def __init__(self,world=World):
        actor.__init__(self)

        self.world = World
        self.icons = {}
        pygame.init()

        window = pygame.display.set_mode((496,496))
        pygame.display.set_caption("Actor Demo")

        self.world.send((self.channel,"JOIN",
                            properties(self.__class__.__name__,
                                       public=False)))

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            self.updateDisplay(msgArgs)
        else:
            print "UNKNOWN MESSAGE", args

    def getIcon(self, iconName):
        if self.icons.has_key(iconName):
            return self.icons[iconName]
        else:
            iconFile = os.path.join("data","%s.bmp" % iconName)
            surface = pygame.image.load(iconFile)
            surface.set_colorkey((0xf3,0x0a,0x0a))
            self.icons[iconName] = surface
            return surface

    def updateDisplay(self,msgArgs):

        for event in pygame.event.get():
            if event.type == pygame.QUIT: sys.exit()

        screen = pygame.display.get_surface()

        background = pygame.Surface(screen.get_size())
        background = background.convert()
        background.fill((200, 200, 200))

        screen.blit(background, (0,0))

        WorldState = msgArgs[0]

        for channel,item in WorldState.actors:
            screen.blit(pygame.transform.rotate(self.getIcon(item.name),-item.angle), item.location)
        pygame.display.flip()

display()

class basicRobot(actor):
    def __init__(self,location=(0,0),angle=135,velocity=1,
                 hitpoints=20,world=World):
        actor.__init__(self)
        self.location = location
        self.angle = angle
        self.velocity = velocity
        self.hitpoints = hitpoints
        self.world = world
        self.world.send((self.channel,"JOIN",
                            properties(self.__class__.__name__,
                                       location=self.location,
                                       angle=self.angle,
                                       velocity=self.velocity,
                                       height=32,width=32,
                                       hitpoints=self.hitpoints)))

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            for actor in msgArgs[0].actors:
                if actor[0] is self: break
            self.location = actor[1].location
            self.angle += 30.0 * (1.0 / msgArgs[0].updateRate)
            if self.angle >= 360:
                self.angle -= 360

            updateMsg = (self.channel, "UPDATE_VECTOR", self.angle,
                         self.velocity)
            self.world.send(updateMsg)
        elif msg == "COLLISION":
            self.angle += 73.0
            if self.angle >= 360:
                self.angle -= 360
            self.hitpoints -= 1
            if self.hitpoints <= 0:
                self.world.send((self.channel, "KILLME"))
        elif msg == "DAMAGE":
            self.hitpoints -= msgArgs[0]
            if self.hitpoints <= 0:
                self.world.send( (self.channel,"KILLME") )
        else:
            print "UNKNOWN MESSAGE", args

basicRobot(angle=135,velocity=150)
basicRobot((464,0),angle=225,velocity=300)
basicRobot((100,200),angle=78,velocity=500)
basicRobot((400,300),angle=298,velocity=5)
basicRobot((55,55),angle=135,velocity=150)
basicRobot((464,123),angle=225,velocity=300)
basicRobot((180,200),angle=78,velocity=500)
basicRobot((400,380),angle=298,velocity=5)

stackless.run()

7.10 actors3.py第三个角色示例

import pygame
import pygame.locals
import os, sys
import stackless
import math
import time
import random

class actor:
    def __init__(self):
        self.channel = stackless.channel()
        self.processMessageMethod = self.defaultMessageAction
        stackless.tasklet(self.processMessage)()

    def processMessage(self):
        while 1:
            self.processMessageMethod(self.channel.receive())

    def defaultMessageAction(self,args):
        print args

class properties:
    def __init__(self,name,location=(-1,-1),angle=0,
                 velocity=0,height=-1,width=-1,hitpoints=1,physical=True,
                 public=True):
        self.name = name
        self.location = location
        self.angle = angle
        self.velocity = velocity
        self.height = height
        self.width = width
        self.public = public
        self.hitpoints = hitpoints
        self.physical = physical

class worldState:
    def __init__(self,updateRate,time):
        self.updateRate = updateRate
        self.time = time
        self.actors = []

class world(actor):
    def __init__(self):
        actor.__init__(self)
        self.registeredActors = {}
        self.updateRate = 30
        self.maxupdateRate = 30
        stackless.tasklet(self.runFrame)()

    def testForCollision(self,x,y,item,otherItems=[]):
        if x < 0 or x + item.width > 496:
            return self.channel
        elif y < 0 or y+ item.height > 496:
            return self.channel
        else:
            ax1,ax2,ay1,ay2 = x, x+item.width, y,y+item.height
            for item,bx1,bx2,by1,by2 in otherItems:
                if self.registeredActors[item].physical == False: continue
                for x,y in [(ax1,ay1),(ax1,ay2),(ax2,ay1),(ax2,ay2)]:
                    if x >= bx1 and x <= bx2 and y >= by1 and y <= by2:
                        return item
                for x,y in [(bx1,by1),(bx1,by2),(bx2,by1),(bx2,by2)]:
                    if x >= ax1 and x <= ax2 and y >= ay1 and y <= ay2:
                        return item
            return None

    def killDeadActors(self):
        for actor in self.registeredActors.keys():
            if self.registeredActors[actor].hitpoints <= 0:
                print "ACTOR DIED", self.registeredActors[actor].hitpoints
                actor.send_exception(TaskletExit)
                del self.registeredActors[actor]

    def updateActorPositions(self):
        actorPositions = []
        for actor in self.registeredActors.keys():
            actorInfo = self.registeredActors[actor]
            if actorInfo.public and actorInfo.physical:
                x,y = actorInfo.location
                angle = actorInfo.angle
                velocity = actorInfo.velocity
                VectorX,VectorY = (math.sin(math.radians(angle)) * velocity,
                                   math.cos(math.radians(angle)) * velocity)
                x += VectorX/self.updateRate
                y -= VectorY/self.updateRate
                collision = self.testForCollision(x,y,actorInfo,actorPositions)
                if collision:
                    #don't move
                    actor.send((self.channel,"COLLISION",actor,collision))
                    if collision and collision is not self.channel:
                        collision.send((self.channel,"COLLISION",actor,collision))
                else:
                    actorInfo.location = (x,y)
                actorPositions.append( (actor,
                                        actorInfo.location[0],
                                        actorInfo.location[0] + actorInfo.height,
                                        actorInfo.location[1],
                                        actorInfo.location[1] + actorInfo.width))

    def sendStateToActors(self,starttime):
        WorldState = worldState(self.updateRate,starttime)
        for actor in self.registeredActors.keys():
            if self.registeredActors[actor].public:
                WorldState.actors.append( (actor, self.registeredActors[actor]) )
        for actor in self.registeredActors.keys():
            actor.send( (self.channel,"WORLD_STATE",WorldState) )

    def runFrame(self):
        initialStartTime = time.clock()
        startTime = time.clock()
        while 1:
            self.killDeadActors()
            self.updateActorPositions()
            self.sendStateToActors(startTime)
            #wait
            calculatedEndTime = startTime + 1.0/self.updateRate

            doneProcessingTime = time.clock()
            percentUtilized =  (doneProcessingTime - startTime) / (1.0/self.updateRate)
            if percentUtilized >= 1:
                self.updateRate -= 1
                print "TOO MUCH LOWERING FRAME RATE: " , self.updateRate
            elif percentUtilized <= 0.6 and self.updateRate < self.maxupdateRate:
                self.updateRate += 1
                print "TOO MUCH FREETIME, RAISING FRAME RATE: " , self.updateRate

            while time.clock() < calculatedEndTime:
                stackless.schedule()
            startTime = calculatedEndTime
            stackless.schedule()

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "JOIN":
            self.registeredActors[sentFrom] = msgArgs[0]
        elif msg == "UPDATE_VECTOR":
            self.registeredActors[sentFrom].angle = msgArgs[0]
            self.registeredActors[sentFrom].velocity = msgArgs[1]
        elif msg == "COLLISION":
            pass # known, but we don't do anything
        elif msg == "KILLME":
            self.registeredActors[sentFrom].hitpoints = 0
        else:
            print '!!!! WORLD GOT UNKNOWN MESSAGE ' , msg, msgArgs

World = world().channel

class display(actor):
    def __init__(self,world=World):
        actor.__init__(self)

        self.world = World
        self.icons = {}
        pygame.init()

        window = pygame.display.set_mode((496,496))
        pygame.display.set_caption("Actor Demo")

        self.world.send((self.channel,"JOIN",
                            properties(self.__class__.__name__,
                                       public=False)))

    def defaultMessageAction(self,args):
        sentFrom, msg, msgArgs = args[0],args[1],args[2:]
        if msg == "WORLD_STATE":
            self.updateDisplay(msgArgs)
        else:
            print "DISPLAY UNKNOWN MESSAGE", args

    def getIcon(self, iconName):
        if self.icons.has_key(iconName):
            return self.icons[iconName]
        else:
            iconFile = os.path.join("data","%s.bmp" % iconName)
            surface = pygame.image.load(iconFile)
            surface.set_colorkey((0xf3,0x0a,0x0a))
            self.icons[iconName] = surface
            return surface

    def updateDisplay(self,msgArgs):

        for event in pygame.event.get():
            if event.type == pygame.QUIT: sys.exit()

        screen = pygame.display.get_surface()

        background = pygame.Surface(screen.get_size())
        background = background.convert()
        background.fill((200, 200, 200))

            screen.blit(background, (0,0))

            WorldState = msgArgs[0]

            for channel,item in WorldState.actors:
                itemImage = self.getIcon(item.name)
                itemImage = pygame.transform.rotate(itemImage,-item.angle)
                screen.blit(itemImage, item.location)
            pygame.display.flip()

    display()

    class basicRobot(actor):
        def __init__(self,location=(0,0),angle=135,velocity=1,
                     hitpoints=20,world=World):
            actor.__init__(self)
            self.location = location
            self.angle = angle
            self.velocity = velocity
            self.hitpoints = hitpoints
            self.world = world
            self.world.send((self.channel,"JOIN",
                                properties(self.__class__.__name__,
                                           location=self.location,
                                           angle=self.angle,
                                           velocity=self.velocity,
                                               height=32,width=32,hitpoints=self.hitpoints)))

            def defaultMessageAction(self,args):
                sentFrom, msg, msgArgs = args[0],args[1],args[2:]
                if msg == "WORLD_STATE":
                    for actor in msgArgs[0].actors:
                        if actor[0] is self: break
                    self.location = actor[1].location
                    self.angle += 30.0 * (1.0 / msgArgs[0].updateRate)
                    if self.angle >= 360:
                        self.angle -= 360

                    updateMsg = (self.channel, "UPDATE_VECTOR",
                                 self.angle,self.velocity)
                    self.world.send(updateMsg)
                elif msg == "COLLISION":
                    self.angle += 73.0
                    if self.angle >= 360:
                        self.angle -= 360
                    self.hitpoints -= 1
                    if self.hitpoints <= 0:
                        explosion(self.location,self.angle)
                        self.world.send((self.channel, "KILLME"))
                elif msg == "DAMAGE":
                    self.hitpoints -= msgArgs[0]
                    if self.hitpoints <= 0:
                        explosion(self.location,self.angle)
                        self.world.send( (self.channel,"KILLME") )

                else:
                    print "BASIC ROBOT UNKNOWN MESSAGE", args

        class explosion(actor):
            def __init__(self,location=(0,0),angle=0,world=World):
                actor.__init__(self)
                self.time = 0.0
                self.world = world
                self.world.send((self.channel,"JOIN",
                                    properties(self.__class__.__name__,
                                               location = location,
                                               angle = angle,
                                               velocity=0,
                                               height=32.0,width=32.0,hitpoints=1,
                                               physical=False)))

            def defaultMessageAction(self,args):
                sentFrom, msg, msgArgs = args[0],args[1],args[2:]
                if msg == "WORLD_STATE":
                    WorldState = msgArgs[0]
                    if self.time == 0.0:
                        self.time = WorldState.time
                    elif WorldState.time >= self.time + 3.0:
                        self.world.send( (self.channel, "KILLME") )

        class mine(actor):
            def __init__(self,location=(0,0),world=World):
                actor.__init__(self)
                self.world = world
                self.world.send((self.channel,"JOIN",
                                    properties(self.__class__.__name__,
                                               location=location,
                                               angle=0,
                                               velocity=0,
                                               height=2.0,width=2.0,hitpoints=1)))

            def defaultMessageAction(self,args):
                sentFrom, msg, msgArgs = args[0],args[1],args[2:]
                if msg == "WORLD_STATE":
                    pass
                elif msg == "COLLISION":
                    if msgArgs[0] is self.channel:
                        other = msgArgs[1]
                    else:
                        other = msgArgs[0]
                    other.send( (self.channel,"DAMAGE",25) )
                    self.world.send( (self.channel,"KILLME"))
                    print "MINE COLLISION"
                else:
                    print "UNKNOWN MESSAGE", args

        class minedropperRobot(actor):
            def __init__(self,location=(0,0),angle=135,velocity=1,
                         hitpoints=20,world=World):
                actor.__init__(self)
                self.location = location
                self.angle = angle
                self.delta = 0.0
                self.height=32.0
                self.width=32.0
                self.deltaDirection = "up"
                self.nextMine = 0.0
                self.velocity = velocity
                self.hitpoints = hitpoints
                self.world = world
                self.world.send((self.channel,"JOIN",
                                    properties(self.__class__.__name__,
                                               location=self.location,
                                               angle=self.angle,
                                               velocity=self.velocity,
                                               height=self.height,width=self.width,
                                               hitpoints=self.hitpoints)))

            def defaultMessageAction(self,args):
                sentFrom, msg, msgArgs = args[0],args[1],args[2:]
                if msg == "WORLD_STATE":
                    for actor in msgArgs[0].actors:
                        if actor[0] is self.channel:
                            break
                    self.location = actor[1].location
                    if self.deltaDirection == "up":
                        self.delta += 60.0 * (1.0 / msgArgs[0].updateRate)
                        if self.delta > 15.0:
                            self.delta = 15.0
                            self.deltaDirection = "down"
                    else:
                        self.delta -= 60.0 * (1.0 / msgArgs[0].updateRate)
                        if self.delta < -15.0:
                            self.delta = -15.0
                            self.deltaDirection = "up"
                    if self.nextMine <= msgArgs[0].time:
                        self.nextMine = msgArgs[0].time + 1.0
                        mineX,mineY = (self.location[0] + (self.width / 2.0) ,
                                       self.location[1] + (self.width / 2.0))

                        mineDistance = (self.width / 2.0 ) ** 2
                        mineDistance += (self.height / 2.0) ** 2
                        mineDistance = math.sqrt(mineDistance)

                        VectorX,VectorY = (math.sin(math.radians(self.angle + self.delta)),
                                           math.cos(math.radians(self.angle + self.delta)))
                        VectorX,VectorY = VectorX * mineDistance,VectorY * mineDistance
                        x,y = self.location
                        x += self.width / 2.0
                        y += self.height / 2.0
                        x -= VectorX
                        y += VectorY
                        mine( (x,y))
                    updateMsg = (self.channel, "UPDATE_VECTOR",
                                 self.angle + self.delta ,self.velocity)
                    self.world.send(updateMsg)
                elif msg == "COLLISION":
                    self.angle += 73.0
                    if self.angle >= 360:
                        self.angle -= 360
                    self.hitpoints -= 1
                    if self.hitpoints <= 0:
                        explosion(self.location,self.angle)
                        self.world.send((self.channel,"KILLME"))
                elif msg == "DAMAGE":
                    self.hitpoints -= msgArgs[0]
                    if self.hitpoints <= 0:
                        explosion(self.location,self.angle)
                        self.world.send((self.channel, "KILLME"))
                else:
                    print "UNKNOWN MESSAGE", args

        class spawner(actor):
            def __init__(self,location=(0,0),world=World):
                actor.__init__(self)
                self.location = location
                self.time = 0.0
                self.world = world

                self.robots = []
                for name,klass in globals().iteritems():
                    if name.endswith("Robot"):
                        self.robots.append(klass)

                self.world.send((self.channel,"JOIN",
                                    properties(self.__class__.__name__,
                                               location = location,
                                               angle=0,
                                               velocity=0,
                                               height=32.0,width=32.0,hitpoints=1,
                                               physical=False)))

            def defaultMessageAction(self,args):
                sentFrom, msg, msgArgs = args[0],args[1],args[2:]
                if msg == "WORLD_STATE":
                    WorldState = msgArgs[0]
                    if self.time == 0.0:
                        self.time = WorldState.time + 0.5 # wait 1/2 second on start
                    elif WorldState.time >= self.time: # every five seconds
                        self.time = WorldState.time + 5.0
                        angle = random.random() * 360.0
                        velocity = random.random() * 1000.0
                        newRobot = random.choice(self.robots)
                        newRobot(self.location,angle,velocity)

        spawner( (32,32) )
        spawner( (432,32) )
        spawner( (32,432) )
        spawner( (432,432) )
        spawner( (232,232) )

        stackless.run()

8 引用链接

stackless

9 给jorge的reStructuredText示例

一段文字

另外一段文字,注意段之间要有空行。

加重的字体使用 两个星号之间 ,当然星号与外界要留有空格。

引用的文字用两个那个符号,比如 我是引用文字 ,同样边界要有空格。这种引用一般用于代码引用,当然这是风格问题,希望你可以与我的风格相同。而下面的反引号引用可以由于函数定义等等非语句性质的引用。至于星号形成的斜体,我一般不用的。

斜体字就比较好说了,单个星号或单个的反引号都是,比如 斜体 或 斜体 。当然,比较规范的斜体是使用星号的。反引号一般用于引用。

链接么就比较简单了,比如 惊帆之静默 注意反引号结束之后有个下划线。然后定义链接地址在下一行。这里的反引号引用用于链接名称里面含有中文和空格的情况,如果没有这些,则可以省略反引号。

关于标题,只要拥有相同下划线的标题就属于同一个级别,当然,上面已经很多例子了。标题与文字段之间也有空行。

代码引用就很酷了,就是在一行的末尾用双冒号,如:

我是代码
def hello():
    print 'hello'
    return

当然,代码所在的块需要缩进表示,结束就是使用原缩进 即可。

图片的引用方式如下所示。

http://图片地址好了就这些吧,高级功能你用到时再问我,比如文章引用,表格等等。

Leave a Reply