| 저자: | 그랜트 올슨(Grant Olson) |
|---|---|
| Email: | olsongt@verizon.net |
| Date: | July 7, 2006 |
스택리스 웹사이트 http://www.stackless.com/에 의하면:
스택리스 파이썬은 파이썬 프로그래밍 언어를 개선한 버전이다. 프로그래머는 전통적인 쓰레드에 연관된 수행성능과 복잡성에 고민하지 않고 쓰레드-기반의 프로그래밍이 주는 과실을 따먹을 수있다. 마이크로쓰레드는 스택리스 파이썬에 추가된 것으로서 편리하고 가볍기 때문에 제대로만 사용하면 다음과 같은 혜택을 누릴 수 있다: + 프로그램 구조의 개선. + 코드의 가독성 개선. + 프로그래머의 생산성 증가.
스택리스 파이썬이 아주 간결하게 정의되어 있지만, 그것이 우리에게 무슨 의미가 있는가? 스택리스 파이썬은 전통적인 언어로 우리가 현재 하고 있는 것보다 훨씬 더 쉽게 병행성을 모델링 할 수 있는 도구를 제공한다. 단순히 파이썬 그 자체만 언급하고 있는 것이 아니다. 자바, C/C++ 그리고 기타 다른 언어 역시 마찬가지다. 병행성이라는 특징을 제공하는 언어들이 몇몇 나와 있지만, 주로 학문적으로 사용되거나 ( Mozart/Oz) 별로 사용되지 않는/특별한 목적의 전문 언어일 뿐이다(erlang). 스택리스 파이썬이면, 병행성과 더불어 파이썬의 장점을 모두 취할 수 있다. (바람직하게도) 이미 친숙한 환경에서 말이다.
물론 여기에는 질문이 제기된다: 왜 병행성이 필요한가?
실세계는 '병행적(concurrent)'이다. 세계는 수 많은 사물(즉 행위자(actors))이 모여 서로 잘 알지 못하면서 상호작용한다. 객체-지향 프로그래밍에서 내세우는 장점도 객체가 실세계의 사물을 잘 모사한다는 것이다. 어느 정도까지는 사실이다. 그러나 개별 객체 사이의 상호작용은 실제로 잘 표현하지 못한다. 예를 들어, 다음 코드는 무엇이 문제일까?
def familyTacoNight():
husband.eat(dinner)
wife.eat(dinner)
son.eat(dinner)
daughter.eat(dinner)
언듯 보면 아무 문제가 없어 보인다. 그러나 위의 예제에는 미묘한 문제가 있다; 사건이 순서대로 일어난다. 다시 말해, 남편이 식사를 끝내기 전까지는 아내는 먹지 못하고, 아들은 엄마가 식사가 끝나야 먹는다. 실세계라면 아버지가 체증에 걸릴지라도 엄마나 아들 그리고 딸이 식사를 할 것이다. 위의 예제대로라면 굶어 죽을 것이다. 더 나쁘다면 아무도 모를 것이다. 예외를 일으키고 세상에 알릴 기회조차 없기 때문이다!
개인적으로 병행성이야 말로 소프트웨어 세계에서 커다란 패러다임이라고 믿는다. 프로그램이 복잡해지고 자원이 집약적이 될 수록, 매년 더 빠른 프로세서가 제공된다는 무어의 법칙에 의존할 수 없다. 현재의 PC 성능 증가는 다중-코어와 다중-시피유 머신 덕분이다. 개인 CPU가 성능을 최대로 발휘하면 다음으로 소프트웨어 개발자는 여러 컴퓨터가 상호작용하여 강력한 어플리케이션을 만드는 분산 모델로 중심을 옮겨야 할 것이다(예 GooglePlex). 다중-코어 머신과 분산 프로그래밍의 장점을 취하려면, 병행성이 사실상 표준적인 방식이다.
스택리스를 설치하는 방법은 웹 사이트에 가보자. 현재, 리눅스 사용자는 서버전으로 소스를 얻어서 구축하면 된다. 윈도우즈 사용자는 .zip 파일을 얻어서 기존의 파이썬 설치본에 풀어 넣으면 된다. 이 자습서는 앞으로 여러분이 스택리스 파이썬을 설치하였고, 기본적으로 파이썬 언어를 알고 있다고 간주한다.
이 장에서는 스택리스의 원자명령어들을 대략 설명한다. 이후로는 이 원자명령어들을 기반으로 하여 좀 더 실용적인 기능들을 보여주겠다.
분할작업은 스택리스의 기본적인 빌딩 블록이다. 분할작업은 파이썬 호출객체를 먹이면 된다 (보통 함수나 클래스 메쏘드). 이렇게 하면 분할작업이 만들어지고 그것을 일정관리기에 추가한다. 다음은 간단한 데모이다:
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()을 호출할 때까지 실행되지 않는 것에 주목하자.
일정관리기는 분할작업이 실행되는 순서를 제어한다. 분할작업을 여럿 만들었다면, 만들어진 순서대로 실행된다. 실제로, 각 분할작업이 자신의 순서를 가지도록 일정을 재조정하는 분할작업을 만들어 보자. 다음은 간단한 데모이다:
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()을 호출하면, 활성 분할작업이 스스로 멈추어서 자신을 일정관리기 대기열의 끝에 다시 끼워넣는다. 다음 태크릿이 실행되도록 말이다. 모든 분할작업이 이렇게 실행되고 나면, 다시 자신이 떠났던 곳에서부터 시작한다. 모든 활성 분할작업이 실행을 완료할 때까지 이렇게 계속된다. 이것이 바로 스택리스 파이썬으로 협력적인 다중작업을 완수하는 방식이다.
통로로 분할작업 사이에 정보를 보낼 수 있다. 이 덕분에 두 가지를 얻을 수 있다:
- 분할작업 사이에 정보를 교환할 수 있다.
- 실행의 흐름을 통제할 수 있다.
짧은 데모를 하나 더 보자:
C:\>c:\python24\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
>>>
>>> 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 >>> >>> # 마지막으로 수신 분할작업을 추가한다 ... >>> stackless.tasklet(receiving_tasklet)() <stackless.tasklet object at 0x00A45BF0> >>> >>> stackless.run() Recieving tasklet started send from sending_tasklet Receiving tasklet finished sending tasklet finished >>>
전송 분할작업은 다른 분할작업에 성공적으로 데이터를 전송할 때까지 일정관리기에 재삽입되지 않는다.
지금까지 스택리스 파이썬이 가진 기능을 대부분 다루었다. 그렇게 많아 보이지 않는가? 두개의 객체와 대략 너댓개의 호출을 다루었다. 이 작은 API를 빌딩블록으로 삼아서 정말로 흥미로운 일을 시작해 보자.
아주 전통적인 프로그래밍 언어에는 서브루틴이라는 개념이 있다. 서브루틴은 다른 루틴에 의해 호출되고 (그 역시 또다른 루틴에서 서브루틴으로 취급됨) 결과를 돌려준다. 정의상, 서브루틴은 호출자에게 종속된다.
다음 예제를 보자:
def ping():
print "PING"
pong()
def pong():
print "PONG"
ping()
ping()
경력 프로그래머라면 이 프로그램의 문제점이 보일 것이다. 즉 스택이 넘친다. 프로그램을 실행해보면, 짜증스런 역추적 메시지가 한가득 보여서 스택 공간이 넘쳤음을 알려줄 것이다.
나는 C-스택에 대하여 얼마나 자세하게 설명할 것인가 고민했다. 그리고 설명을 하지 않기로 결정했다. 다이어그램을 그려서 자세하게 설명하려는 그런 시도는 이미 그 개념을 알고 있는 사람들에게만 의미가 있다. 여기에서는 최대한 단순하게 설명하도록 하겠다. 관심있는 독자는 웹에서 좀 더 정보를 찾아 보시기를 바란다.
서브루틴을 호출할 때마다, 스택 프레임이 생성된다. 스택 프레임은 그 서브루틴에 있는 변수와 기타 정보를 모아두는 곳이다. 이 경우, ping()을 호출하면 스택 프레임이 생성되고 서브루틴 호출에 대하여 정보를 모아둔다. 간단하게 말해서, 프레임은 ping pinged이라고 알린다. pong()을 호출하면, 또다른 스택이 만들어져서 pong ponged라고 알린다. 스택 프레임은 사슬과 같이 연결되어, 서브루틴을 호출할 때마다 사슬에서 연결점으로 작용한다. 이 경우, 스택은 기본적으로 ping pinged so pong ponged를 돌려준다. 물론 pong()이 ping()을 호출하면 스택은 확장된다. 다음은 시각적으로 표현해 본 것이다:
| 프레임 | 스택 |
|---|---|
| 1 | ping pinged |
| 2 | ping pinged so pong ponged |
| 3 | ping pinged so pong ponged so ping pinged |
| 4 | ping pinged so pong ponged so ping pinged so pong ponged |
| 5 | ping pinged so pong ponged so ping pinged so pong ponged so ping pinged |
| 6 | ping pinged so pong ponged so ping pinged so pong ponged so ping pinged ... |
이제 생각해보자. 페이지 너비가 메모리를 나타내며 시스템은 그것을 스택에 할당한다. 페이지의 끝에 다다르면, 넘쳐 흐르게 되고(overflow) 시스템은 메모리가 고갈된다. 그러므로 스택 범람(stack overflow)이라고 부른다.
위의 예제에는 의도적으로 설계되어 스택의 문제점을 보여준다. 대부분의 경우, 각 스택 프레임은 서브루틴이 반환되면 스스로 깨끗이 비운다. 이는 일반적으로는 좋은 일이다. C에서, 스택은 프로그래머가 수작업으로 메모리를 관리할 필요가 없는 곳이다. 다행스럽게도, 파이썬 프로그래머는 메모리 관리와 스택 관리를 직접적으로 신경쓸 필요가 없지만, 파이썬 인터프리터 자체가 C로 작성되어 있기 때문에, 구현자는 신경쓸 필요가 있다. 스택을 사용하면 일이 편해진다. 그러나 위의 예제와 같이 절대 반환되지 않는 함수를 호출하기 시작하면 상황은 달라진다. 그러면 스택은 프로그래머의 의도에 반하여 작동하기 시작하여, 가용 메모리를 모두 소진해 버린다.
이 경우, 스택이 범람하는 것은 약간 우습다. ping()과 pong()은 실제로는 서브루틴이 아니기 때문이다. 서로 종속되어 있지 않다. 이 두 함수는 동급루틴(coroutines)이며, 나란히 걸어가며 서로 흠없이 대화를 나눌 수 있어야 한다.
| 프레임 | 스택 |
|---|---|
| 1 | ping pinged |
| 2 | pong ponged |
| 3 | ping pinged |
| 4 | pong ponged |
| 5 | ping pinged |
| 6 | pong ponged |
스택리스 파이썬으로 통로가 있는 동급루틴을 작성해 보자. 기억하시겠지만, 통로의 두가지 혜택중 하나가 분할작업 사이에 흐름을 통제할 수 있다는 것이다. 통로를 사용하면, ping과 pong 사이를 스택 범람없이 얼마든지 오갈 수 있다:
#
# 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()
이 프로그램을 충돌없이 얼마든지 오랫동안 실행할 수 있다. 또한, (윈도우즈라면 태스크 매니저로 또는 유닉스라면 top으로) 메모리 관리를 들여다 보면, 메모리 관리가 항상 일정한 것을 보실 것이다. 동급루틴 버전의 프로그램에서는 1분을 실행하든 하루를 실행하든 같은 메모리를 사용한다. 재귀적 버전의 메모리 사용을 조사해 보면, 그 즉시 늘어나서 프로그램이 폭발하는 것을 보실 것이다.
기억하시겠지만, 이미 언급했듯이 경력 프로그래머라면 재귀적 버전의 코드에서 즉시 문제점을 볼 수 있을 것이다. 그러나 솔직히 말해, 이런 코드를 작동하지 못하도록 통제하는 것은 '컴퓨터 과학'에서 고려사항이 아니다. 전통적인 언어를 지키고 있다면 그냥 사소한 자잘한 구현일 뿐인데, 전통적인 언어는 대부분 스택을 사용하기 때문이다. 어떤 면에서, 경력 프로그래머는 그냥 이런 문제점을 받아들일 수 있는 것이라고 세뇌되어 왔다고 할 수 있다. 스택리스 파이썬은 이런 문제점을 해결했다
이름이 의미하듯이 분할쓰레드는 오늘날의 운영체제에 내장된 쓰레드와 표준 파이썬 코드에서 제공하는 쓰레드에 비하여 가볍다. 분할쓰레드는 '전통적인 쓰레드'보다 메모리를 덜 사용하고, 분할쓰레드 사이를 오가는 것은 전통적인 쓰레드에 비하여 자원을 훨씬 덜 사용한다.
전통적인 쓰레드에 비하여 분할쓰레드가 얼마나 더 효율적인지 보여주기 위하여, 같은 프로그램을 전통적인 쓰레드와 분할쓰레드를 사용하여 작성해 보자.
콩주머니차기(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)
hackysacker 클래스는 자신의 이름과 전역 리스트 circle에 대한 참조점 그리고 메시지 큐로 초기화된다. circle에 선수 모두를 담고 있고 메시지 큐는 파이썬 표준 라이브러리에 포함된 Queue 클래스로부터 파생된다.
Queue 클래스는 스택리스 파이썬의 통로처럼 행위한다. 거기에는 put() 메쏘드와 get() 메쏘드가 포함되어 있다. 빈 대기열에 put() 메쏘드를 요청하면 또다른 쓰레드가 무언가를 대기열에 put()을 요청할 때까지 블록된다. Queue 클래스는 운영체제-수준의 쓰레드와도 효율적으로 작동하도록 설계되었다.
__init__ 메쏘드는 이제 표준 라이브러리에 있는 thread 모듈을 사용하여 새로운 쓰레드 안에서 messageLoop를 시작한다. 메시지 회돌이는 무한 회돌이를 시작하고 대기열에 있는 메시지를 처리한다. 특별한 메시지 'exit'를 받으면, 쓰레드를 종료한다.
또다른 메시지를 받는다면 그것은 콩주머니를 받았다는 뜻이다. 회돌이로부터 무작위로 다른 선수를 뽑아서 그 선수에게 메시지를 보냄으로써 콩주머니를 '차 넘긴다'.
찰 때마다 그 횟수가 클래스 변수 hackysacker.counter에 기록되며, 한껏 차고나면 원 안에 있는 각 선수에게 특별한 'exit' 메시지가 전송된다.
debugPrint 함수도 있음을 주목하자 전역 변수 디버그가 0이 아니면 출력을 인쇄한다. 게임을 표준출력으로 인쇄할 수 있지만, 그렇게 되면 시간 측정이 부정확하게 될 것이다.
프로그램을 실행해서 잘 작동하는지 검증하자:
C:\Documents and Settings\grant\Desktop\why_stackless\code>c:\python24\python.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 Settings\grant\Desktop\why_stackless\code>
보시다시피 선수가 모두 함께 모여 놀이를 한다. 이제 시험 실행을 시간 측정해보자. 표준 파이썬 라이브러리에 timeit.py이 있다. 여기에서는 디버그 인쇄도 불능화시키겠다:
C:\Documents and Settings\grant\Desktop\why_stackless\code>c:\python24\python.ex e c:\Python24\lib\timeit.py -s "import hackysackthreaded" hackysackthreaded.ru nit(10,1000,0) 10 loops, best of 3: 183 msec per loop
10명의 해키새커가 1000회를 진행하는데 183 밀리초가 걸린다. 선수의 수를 늘려보자:
C:\Documents and Settings\grant\Desktop\why_stackless\code>c:\python24\python.ex
e c:\Python24\lib\timeit.py -s "import hackeysackthreaded" hackeysackthreaded.ru
nit(100,1000,0)
10 loops, best of 3: 231 msec per loop
C:\Documents and Settings\grant\Desktop\why_stackless\code>c:\python24\python.ex
e c:\Python24\lib\timeit.py -s "import hackysackthreaded" hackysackthreaded.ru
nit(1000,1000,0)
10 loops, best of 3: 681 msec per loop
C:\Documents and Settings\grant\Desktop\why_stackless\code>c:\python24\python.ex
e c:\Python24\lib\timeit.py -s "import hackysackthreaded" hackysackthreaded.ru
nit(10000,1000,0)
Traceback (most recent call last):
File "c:\Python24\lib\timeit.py", line 255, in main
x = t.timeit(number)
File "c:\Python24\lib\timeit.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
메모리가 1기가이고 시피유가 3 Ghz인 나의 컴퓨터에서 10,000 쓰레드를 시도하자 에러가 났다. 자세하게 출력하여 여러분을 괴롭히고 싶지는 않지만, 몇번의 시행착오 끝에 프로그램은 대략 1100 쓰레드에서 에러를 일으키기 시작한다. 또 주목하자. 1000개의 쓰레드가 10개짜리 쓰레드에 비하여 대략 3배를 더 사용한다.
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()
이 코드는 사실상 쓰레드 버전과 동일하다. 근본적인 차이는 쓰레드가 아니라 분할작업으로 시작해서 큐 객체 말고 통로를 통하여 쓰레드 사이를 전환한다는 것이다. 실행해서 출력을 검증해 보자:
C:\Documents and Settings\grant\Desktop\why_stackless\code>c:\Python24\python.ex e 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 Settings\grant\Desktop\why_stackless\code>c:\Python24\python.ex e c:\Python24\lib\timeit.py -s"import hackysackstackless" hackysackstackless.r unit(10,1000,0) 100 loops, best of 3: 19.7 msec per loop
겨우 19.7 밀리초가 걸렸다. 쓰레드 버전보다 거의 10배가 빠르다. 분할쓰레드의 개수를 늘려보자:
C:\Documents and Settings\grant\Desktop\why_stackless\code>c:\Python24\python.ex e c:\Python24\lib\timeit.py -s"import hackysackstackless" hackysackstackless.r unit(100,1000,0) 100 loops, best of 3: 19.7 msec per loop C:\Documents and Settings\grant\Desktop\why_stackless\code>c:\Python24\python.ex e c:\Python24\lib\timeit.py -s"import hackysackstackless" hackysackstackless.r unit(1000,1000,0) 10 loops, best of 3: 26.9 msec per loop C:\Documents and Settings\grant\Desktop\why_stackless\code>c:\Python24\python.ex e c:\Python24\lib\timeit.py -s"import hackysackstackless" hackysackstackless.r unit(10000,1000,0) 10 loops, best of 3: 109 msec per loop C:\Documents and Settings\grant\Desktop\why_stackless\code>c:\Python24\python.ex e c:\Python24\lib\timeit.py -s"import hackysackstackless" hackysackstackless.r unit(100000,1000,0) 10 loops, best of 3: 1.07 sec per loop
쓰레드 버전에서는 실행조차 불가능했던 10,000 쓰레드에 도달할 때까지도, 겨우 10개의 쓰레드일 때의 쓰레드 버전보다 여전히 더 빠르다.
코드를 간단하게 유지하고자 했으므로, 내 말을 믿을 수 있을 것이다. 그러나 여기에서 시간의 증가는 콩주머니차기 원을 설정하는데 시간이 걸리기 때문이다. 게임 실행에 드는 시간은 10개짜리 분할쓰레드이든 100000개짜리 분할쓰레드이든 상관없이 일정하다. 이는 통로의 작동방식 때문이다: 통로는 메시지를 받으면 블록되고 즉시 재개된다. 반면에, OS 쓰레드는 각각 차례를 기다려서 메시지 큐에 원소가 있는지 점검한다. 이는 쓰레드가 많이 실행될 수록 수행성능은 그 만큼 더 나빠진다는 뜻이다.
모쪼록, 내가 잘 설명했기를 바란다. 분할쓰레드는 OS 쓰레드보다 적어도 한 자리수 이상 빠르며, 훨씬 더 신축성이 좋다. 일반적으로 OS 쓰레드에 대해서 알아야 할 것은 (1) 사용하지 말 것, 그리고 (2) 가능하면 적게 사용하라는 것이다. 스택리스 파이썬의 분할쓰레드는 이런 제약으로부터 자유롭다.
다음과 같은 조건이 있는 인형 공장을 시뮬레이션 하고 싶다고 해보자:
- 성형에 사용될 플라스틱 조각들이 가득 담긴 창고방 하나.
- 부품들을 조립하는데 사용될 리벳이 담긴 창고방 하나.
- 0.2 파운드의 플라스틱 조각들을 취하는 사출 성형기. 6초에 한쌍의 팔을 만들어낸다.
- 0.2 파운드의 플라스틱 조각들을 취하는 사출 성형기. 5초에 한쌍의 다리를 만들어낸다.
- 0.1 파운드의 플라스틱 조각들을 취하는 사출 성형기. 4초에 머리 하나를 만들어낸다.
- 0.5 파운드의 플라스틱 조각들을 취하는 사출 성형기. 10초에 몸통 하나를 만들어낸다.
- 완성된 몸통과 다리 한쌍 그리고 리벳을 모아서 2초에 조립하는 조립장 하나.
- 위에서 넘어온 부품에 한쌍의 팔과 리벳을 모아서 2초에 조립하는 조립장 하나.
- 위에서 넘어온 부품에 머리와 리벳을 모아서 3초에 조립하는 조립장 하나.
- 각 조립장은 끝없이 이 일을 지속한다.
이것을 스택리스 없이 '평범하게' 작성해 보겠다. '보통의' 예제를 보고 난 후, 스택리스로 똑 같이 구축해서 결과 코드를 비교해 보겠다. 예제가 작위적이라 생각되고 시간이 충분하다면, 여러분이 잠시 시간을 내서 위의 요구조건에 맞는 공장을 손수 작성해서 스택리스 버전과 결과 코드를 비교해 보셔도 좋다.
다음이 그 코드이다:
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 "\n\n\n"
if __name__ == "__main__":
run()
창고방을 나타내는 클래스에서부터 시작한다. 창고방은 제품 이름과 (파운드나 부품 개수 같은) 측정 단위 그리고 최초량으로 초기화된다. 아무것도 하지 않는 실행 메쏘드도 있다; 그의 사용법은 나중에 설명하겠다. 두개의 창고방 실체를 이 클래스에 근거하여 만든다.
다음은 injectionMolder 클래스이다. 이 클래스는 완성 부품의 이름 그리고 플라스틱 재료로 작용하는 창고방 하나 그리고 한 부품을 완성하는데 요구되는 양 그리고 부품을 생성하는데 필요한 시간으로 초기화된다. get() 메쏘드는 완성 부품을 열람하고 완성품이 존재하면 재고량을 조율하는데 사용된다. 이 클래스에 대해서는 run() 메쏘드가 실제로 일을 해준다:
- 타이머가 0 이상인 동안, 계속해서 성형하면서 계수기를 감소시킨다.
- 성형시간(time-to-mold)이 0에 다다르면, 부품이 하나 완성되고 시간 계수기에서 1을 뺀다.
- 시간 계수기가 -1에 도달하면, 성형기는 충분히 플라스틱이 있어서 또 부품을 만들 수 있는지 점검하고, 필요하면 재료를 가져와서, 성형을 시작한다.
이 클래스로 사출 성형기를 네개 만들었다.
다음은 조립 클래스를 보자. 이 클래스는 완성 부품의 이름과, 부품 1을 위한 소스, 부품 2를 위한 소스, 리벳이 있는 창고방, 해당 부품들을 조립하는데 드는 시간으로 초기화된다. get() 메쏘드는 완성된 부품을 열람하고 필요하면 재고량을 조율하는데 사용된다. 이 클래스를 위하여 run() 메쏘드가 하는 일은 다음과 같다:
- 타이머가 1 이상이면, 조립기는 필요한 부품을 가지고 조립을 계속한다.
- 타이머가 0과 같아지면, 부품 하나가 완성되고 재고량이 조정된다.
- 타이머가 1보다 작아지면, 조립기는 요구되는 각 부분을 가져와서 다시 조립을 시작한다. 아직 부품이 성형되지 못했다면 여기에서 잠시 기다릴 수도 있다.
조립기 실체는 다리와 팔 머리를 붙이도록 생성된다.
주의
창고방, 사출성형기 그리고 조립기 클래스 사이가 상당히 비슷한 것을 보실 수 있을 것이다. 생산 시스템을 작성하려고 했다면 아마도 나는 바탕 클래스를 하나 만들고 상속을 사용했을 것이다. 여기에서는 클래스 계층도를 설정하면 코드가 혼란스러울 것 같다고 생각했고, 그래서 의도적으로 단순하게 유지했다.
이 세 클래스에서 파생된 실체들은 모두 components라고 불리우는 배열 안으로 적재된다. 그러면 이벤트 회돌이를 만들어서 반복적으로 각 구성요소에 대하여 run() 메쏘드를 호출할 수 있다.
유닉스 시스템을 잘 알고 있다면, 알게 모르게 데이터흐름 테크닉을 사용해 보셨을 것이다. 다음의 쉘 명령어를 보자:
cat README | more
공평하게, 윈도우즈에서도 같은 것이 있다:
type readme.txt | more
물론 데이터흐름 테크닉은 유닉스 세계만큼 윈도우즈 세계에서는 널리 퍼져 있지 않다.
more 프로그램을 잘 모르는 분들을 위해 설명하면, 이 프로그램은 외부 소스로부터 입력을 받아 한 페이지 분량의 출력만 보여주고, 사용자가 키를 눌러야만 또다른 페이지를 보여준다. '|' 연산자는 한 프로그램으로부터 건네지는 출력을 받아서, 그것을 파이프에 넣어 또다른 명령어의 입력으로 건넨다. 이 경우에는 cat 또는 type가 한 문서의 텍스트를 표준 출력으로 보내고, more가 그것을 받는다.
more 프로그램은 그냥 앉아서 데이터가 또다른 프로그램으로부터 흘러 들어올 때까지 기다린다. 충분히 데이터가 흘러 들어오면, 화면에 페이지를 하나 보여주고 멈춘다. 사용자는 키를 하나 치고, 그러면 more 프로그램은 데이터를 더 흘러들어오게 한다. 다시 또, more는 충분히 데이터가 들어올 때가지 기다리며, 페이지를 화면에 표시하고 나서 멈춘다. 그러므로 데이터흐름(dataflow)이라고 부른다.
통로와 더불어 스택리스 파이썬의 라운드-로빈 일정관리기를 사용하면, 데이터흐름 테크닉을 사용하여 공장을 시뮬레이션하는 코드를 작성할 수 있다.
import stackless
#
# 'Sleep helper functions
#
sleepingTasklets = []
sleepingTicks = 0
def Sleep(secondsToWait):
channel = stackless.channel()
endTime = sleepingTicks + secondsToWait
sleepingTasklets.append((endTime, channel))
sleepingTasklets.sort()
# Block until we get sent an awakening notification.
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]
# We have to send something, but it doesn't matter
# what as it is not used.
channel.send(None)
endTime = sleepingTasklets[0][0] # check next
sleepingTicks += 1
print "1 second passed"
stackless.schedule()
stackless.tasklet(ManageSleepingTasklets)()
#
# Factory implementation
#
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: #reschedule until we have enough
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: #reschedule until we have enough
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 "\n\n\n"
stackless.schedule()
stackless.tasklet(pause)()
def run():
stackless.run()
if __name__ == "__main__":
run()
먼저 도움자 함수를 만들어서 클래스가 'sleep'하도록 허용한다. 분할작업이 Sleep()를 호출하면, 도움자 함수는 통로를 만들고, 깨는데 걸리는 시간을 계산한 다음 이 정보를 전역 sleepingTasklets 배열에 붙인다. 그 다음, channel.receive()가 호출된다. 이렇게 하면 호출 분할작업이 다시 깰 때까지 멈춘다.
다음으로 수면 분할작업을 관리할 함수를 만든다. 이 함수는 전역 sleepingTasklets 배열을 점검해서, 깰 준비가 된 항목들이 있는지 알아보다. 그리고 통로를 통하여 다시 활성화시킨다. 이 함수는 분할작업 일정관리기에 추가된다.
이 클래스들은 눈에 띠는 몇 가지 점만 제외하면 원래의 클래스와 비슷하다. 첫째는 실체화될 때 run() 메쏘드를 깔아 준다는 것이다. 이벤트 회돌이를 처리하기 위하여 더 이상 수작업으로 구성요소 배열과 외부 run() 함수를 만들 필요가 없다; 스택리스 파이썬이 묵시적으로 이 일을 처리한다. 두 번째 차이점은 부품이 완성될 때까지 분할작업이 잠자면서 기다린다는 것이다. 시간을 추적하기 위하여 명시적으로 계수기를 관리하지 않고서 말이다. 세 번째 차이점은 get()를 더 자연스럽게 호출한다는 것이다. 재료 또는 부품이 모자라면, 분할작업은 공급될 때까지 그냥 자신의 일정을 조정한다.
좋다. 그래서 뭐가 어떻다는 뜻인가? 두 프로그램 모두 기본적으로 같은 결과를 만들어낸다. 원래 버전의 공장에 있는 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
다음으로 스택리스 버전을 살펴보자:
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()
스택리스 버전이 원래 버전보다 더 간단하고 더 명료하며 직관적이다. 스택리스 버전은 이벤트 회돌이 기반구조를 run 메쏘드 안에 싸넣지 않는다. 이 기반구조는 run() 메쏘드와 분리되었다. run() 메쏘드는 자신이 무엇을 하고 있는지 나타낼 뿐, 일이 어떻게 진행되는지 자세한 사항에 대해서는 신경쓰지 않는다. 그 덕분에 소프트웨어 개발자는 이벤트 회돌이와 프로그램 작동 방식이 아니라 공장이 작동하는 방식에 집중할 수 있다.
주의
이 섹션에서 완성된 프로그램은 이 문서의 끝에 digitalCircuit.py라는 이름으로 코드와 .zip 압축파일의 형태로 나열되어 있다.
위의 공장의 경우에 데이터를 '끌어 오고 있다'. 각 구성요소는 필요한 부품을 요구하고 부품이 도착할 때까지 기다린다. 정보를 '밀어 보낼 수도 있다'. 한 시스템에서 구성요소는 자신의 변동사항을 또다른 구성요소로 전파할 수 있다. '끌어오기' 접근법은 수동적 데이터 흐름(lazy data flow)이라고 부르며 '밀어보내기' 접근법은 능동적 데이터 흐름(eager data flow)이라고 불리운다.
밀어보내기 접근법을 보여주기 위해 디지털 회로 시뮬레이터를 만들어보겠다. 이 시뮬레이터는 다양한 부품으로 구성되어 있는데 상태가 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 클래스의 핵심 기능은 다음 세가지를 수행하는 것이다:
- 듣기 메쏘드로 통로에 메시지가 있는지 계속해서 경청한다.
- 다음 processMessage 메쏘드를 통하여 메시지를 받으면 처리한다.
- 다음 결과가 등록된 출력이면 고지 메쏘드를 통하여 고지한다.
또 도움자 메쏘드가 두개 더 있다:
registerOutput으로 실체화후에 추가로 출력을 더 등록할 수 있다.
__call__는 편의상 오버라이드 되어 메시지를 다음과 같은 형태로 보낼 수 있다:
event(1)다음과 같이 하는 대신에 말이다:
event.channel.send(1)
EventHandler 클래스를 빌딩블록으로 사용하면 디지털 회로 시뮬레이터를 구축할 수 있다. Switch부터 시작해보자. 이 스위치는 사용자가-토글하는 스위치로서 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 메쏘드는 실체 자신과 상태에 대한 참조점을 담고 있는 터플을 보내도록 오버라이드된다. 조금 있으면 보게 되겠지만, 실체를 보낼 필요가 있는데, 입력이 여럿인 구성요소가 메시지의 소스가 어디인지 구분할 수 있도록 하기 위해서다.
주의
여러분이 읽어가면서 바로바로 코드를 타자해 넣고 있다면, 원래 경량 쓰레드 섹션에서 정의된 debugPrint() 함수를 사용해서 진단하고 있음을 주의하자.
다음으로 만들 클래스는 Reporter() 클래스이다. 이 클래스의 실체는 그냥 자신의 현재 상태를 화면에 표시한다. 이것들을 실제 디지털 회로에서 LED라고 생각하면 되겠다:
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 Settings\grant\Desktop\why_stackless\code>c:\Python24\python.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) #create switch and attach reporter as output. >>> >>> 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 클래스처럼, 고지 이벤트는 자신과 자신의 상태가 담긴 터플을 보낸다.
위에 있는 예제의 Switch와 Reporter 사이에 이 인버터를 사슬로 하여 엮을 수 있다. 원하신다면 얼마든지 시험해 보셔도 좋다. 여기에서는 상호대화 세션을 보여줄 필요가 없다고 생각했다.
다음은 AndGate를 만들어보자. 이 회로는 여러 입력을 받는 첫 클래스이다. 입력은 두 개이다. 둘 다 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))
AndGate 회로의 처리 메시지에서 어떤 입력이 메시지를 보냈는지 결정해서 그 상태를 적절하게 할당할 필요가 있다. 이 때문에 다른 구성요소로부터 셀프 객체를 보낼 필요가 있는 것이다.
마지막으로, OrGate를 살펴보자. AndGate와 동일하다. 단 두 입력이 모두 0일 경우 메시지 0을 전송하고, 입력중 하나가 1이기만 하면 메시지 1을 전송한다는 점만 다르다:
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__":
# 반 가산기
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)
액터 모델에서, 모든 것은 행위자이다 (설마!). 행위자와 객체는 (일반적인 의미에서, 꼭 객체 지향적인 의미일 필요는 없음) 다음과 같은 일을 할 수 있다:
- 다른 행위자로부터 메시지를 수신한다.
- 수신된 메시지를 마음대로 처리한다.
- 메시지를 다른 행위자에게 전달한다.
- 새로운 행위자를 만든다.
행위자는 직접적으로 다른 행위자에 접근하지 않는다. 모든 통신은 메시지 건네기로 이루어진다. 이 덕분에 실-세계 객체를 시뮬레이션하는 모델이 풍부해진다. 실세계의 객체들은 느슨하게 연결되어 서로 내부적인 일은 잘 알지 못한다.
시뮬레이션을 만들려면, 제대로 시뮬레이션 하는 편이 좋다...
주의
이 섹션에서 완성된 프로그램은 이 문서의 끝에 actors.py라는 이름으로 코드와 .zip 압축파일의 형태로 첨부되어 있다.
이 예제에서는 액터 모델을 활용하여, 로보트가 돌아다니면서 싸우는 작은 세계를 구성해 보고자 한다. 먼저, 모든 행위자를 위한 바탕 클래스를 정의해 보자:
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
기본으로 행위자는 통로를 만들어서 메시지를 수신하고, 그 메시지를 처리하기 위한 메쏘드를 할당하며, 수신된 메시지를 회돌이를 돌면서 처리 메쏘드에 분배한다. 기본 처리자는 그냥 수신된 메시지를 인쇄한다. 이정도면 실제로 액터 모델을 구현하기에 충분하다.
모든 메시지는 전송자의 통로의 형태로 전송되며, 다음에 메시지 이름을 담고 있는 문자열이 따르며, 다음으로 선택적인 매개변수들이 따른다. 예를 들면 다음과 같다:
(self.channel, "JOIN", (1,1) ) (self.channel, "COLLISION") etc...
전체 셀프 객체가 아니라 전송자의 통로만 보내고 있음을 주목하자. 액터 모델에서는 행위자들 사이의 모든 통신이 메시지 건네기를 통하여 이루어져야 한다. self를 건넨다면, 메시지를 보내는 행위자에 관한 비밀 정보에 접근해서 속이기가 쉬울 것이다.
실제로 눈치채셨을 것이다. 이 장에서 행위자를 실체화할 때 대부분 그들을 변수에 조차 할당하지 않고 있다. 변수는 다른 행위자가 접근가능하다. 행위자는 환경에 관해서는 조금만 안 채로 단순히 생성되어 떠돌아 다닐 뿐이다.
세계 행위자는 모든 행위자가 상호작용하는 중앙 처리소로 작용한다. 다른 행위자들은 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 메시지를 전송하도록 그의 통로가 전역 변수 World에 저장된다.
일정한 속력으로 움직이는 간단한 로보트으로 시작해 보자. 이 로보트는 각각의 WORLD_STATE 메시지에 응답하여 시계 방향으로 1도씩 회전한다. 세계의 벽에 충돌하면, 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()
주목하자. 로보트에 대한 구성자는 세계 객체에 참여 메시지를 제출하여 자신을 등록한다. 그것만 빼면, 코드는 보이는 그대로 이해된다.
지금까지 디버그용 인쇄 서술문을 이용해서 샘플 프로그램이 작동하는 방식을 보여주었다. 코드는 그대로 단순하고 쉽게 두고서 보여주려고 노력했지만, 어떤 시점을 넘어서면 print 서술문이 이해를 돕기 보다는 혼란을 가중시킨다. 이미 데이터흐름에 관한 섹션에서 사용하고 있지만, 이 섹션에서는 그 코드가 너무 복잡해서 인쇄 출력만으로는 표현하기조차 힘들다.
주의
이 섹션에서 코드 샘들을 작동시키려면 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를 받아 그에 근거하여 화면을 만든다.
주의
이 섹션에서 다루는 예제들을 작동시키려면 파이썬 배포본에 pyGame을 설치할 필요가 있다. 또 선택적으로 본인이 만든 아이콘을 내려받아 소스 디렉토리에 압축을 풀어 넣어도 좋겠다.
이제 첫 프로그램을 작성해도 충분하다. 실행하면, 두 개의 basicRobots이나 나타나서 벽에 튀어 돌아다닐 것이다.
주의
이 섹션에서 완성된 프로그램은 actors2.py라는 이름으로 이 문서의 끝에 그 코드와 .zip 압축파일이 첨부되어 있다.
또다른 샛길로 빠져서 게임 (음... 시뮬레이션이는 뜻) 메커니즘을 구현해 보자. 엄격하게 말해서, 이 매커니즘은 행위자 모델과 전혀 관련이 없다. 그렇지만, 풍부하고 현실적인 시뮬레이션을 만들려면 이런 메커니즘을 벗어날 필요가 있다. 이 섹션에서는 우리가 얻고자 하는 것과 얻는 방법을 자세하게 기술한다. 그러고 나면, 행위자를 갖고 노는 우리의 환경은 더욱 더 쓸모가 있게 된다.
세계 행위자가 추적할 필요가 있는 정보가 복잡해질 수록, 최초의 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
특성 객체를 만들어서 행위자들 사이에 정보를 전송하는 것에 주목하자. 정보를 만들어내는 행위자의 사본을 지역적으로 보관하지 않는다. 지역적으로 보관하면, 세계 행위자는 메시지를 전송함으로써 적절하게 바꾸는 대신에 행위자의 내부 작동방식을 바꿀 수 있다.
지난 프로그램에서는 충돌 탐지 루틴에 몇가지 문제가 있다. 확실한 문제는 행위자가 서로 충돌하지 않는다는 것이다. 튀어 돌아다니는 두 로보트는 충돌하지 않고 그냥 서로 통과해 버린다. 두번째 문제는 행위자의 크기를 고려하지 않았다는 것이다. 두 로보트가 오른쪽 벽이나 아래 벽에 부딪치면 이 문제점이 극명하게 들어난다. 로보트는 벽 속으로 반이 들어간 후에야 충돌이 등록된다. 온전히 충돌 탐지만으로 지면을 가득 채운 책들이 많이 나와 있지만, 우리 목적에 맞게 잘 작동할 만큼만 적당히 간단한 버전으로 만들어보자.
먼저, 각각의 행위자에 높이와 너비라는 특성을 추가하겠다. 이렇게 하면 그 행위자를 둘러싼 '포장상자'를 만들 수 있다. 위치 특성에는 상자의 좌-상 모서리의 위치가 담기고, 이 값에 너비와 높이를 추가하면 상자의 우하 위치가 만들어진다. 이 정도면 행위자의 물리적 부피에 필적한다.
세계 벽에의 충돌을 탐지하려면 이제 포장 상자의 모서리가 세계의 모서리에 충돌하는지 알아 보면 된다. 다른 객체와의 충돌을 탐지하기 위해 충돌 탐지를 이미 끝낸 항목을 리스트로 보관하겠다. 그 리스트를 순회하면서 행위자중의 하나로부터 모서리-점의 위치가 또다른 행위자의 포장 상자 안에 들어 있는지 알아본다. 그렇다면 충돌한 것이다.
이것이 바로 기본적인 충돌 탐지 시스템에 필요한 모든 것이다. 개별 충돌을 탐지하는 함수는 다음과 같다:
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:
#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))
우리의 시뮬레이션에서 또라는 문제점은 서로 다른 컴퓨터에서 서로 속도가 다르다는 것이다. 여러분의 컴퓨터가 본인의 컴퓨터보다 빠르다면, 로보트를 거의 볼 수 없을 것이다. 더 느리다면, 로보트들이 기어다닐 것이다.
이를 교정하기 위해 WORLD_STATE 메시지를 일정한 비율로 제출하겠다. 기본으로 1/30초에 한 번씩 제출하겠다. 그에 관하여 표준화를 할 수 있으면, 일이 좀 쉽겠지만, 컴퓨터가 과중한 부담을 제대로 처리하지 못하고 이 갱신 비율을 유지하지 못한다면 교정할 수 있어야 한다.면 (프로그램이 복잡하거나, 외부 프로그램이 자원을 잡고 있기 때문에) 1/30초가 더 걸려야 프레임 하나를 실행할 수 있다. 갱신 비율을 조정할 필요가 있다.
우리의 예제에서 갱신 비율에 근거한 시간보다 더 많은 시간을 들여야만 목적을 달성한다면 그 비율을 조금씩 줄인다. 현재의 갱신 비율에 근거하여 대략 40% 이상 시간에 여유가 있다면, 조금씩 그 비율을 증가시켜 초당 최대 30회 갱신하도록 하겠다.
이렇게 하면 다른 컴퓨터에서도 같은 속도로 실행할 수 있다. 그러나 흥미로운 문제가 제기된다. 예를 들어, 현재는 갱신할 때마다 basicRobot의 각도를 1도씩 갱신하고 갱신할 때마다 속도를 설정한다. 이 프로그램을 10초 동안 다른 컴퓨터에서 실행하면, 하나는 초당 20 번을 갱신하고 다른 하나는 초당 30 번을 갱신하게 되어, 로보트의 위치가 서로 달라진다. 이것은 바람직하지 못하다. 행위자들이 증분-시간에 근거하여 만든 갱신 비율을 조정할 필요가 있다.
basicRobots의 경우, 화면이 바뀔 때마다 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)
#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()
지금 로보트는 불사신이다. 끝까지 살아 남을 것이다. 그것은 별로 재미가 없다. 손상을 받은 만큼 생명이 단축되면 좋겠다. 이를 활용하려면 두개의 메시지를 새로 추가할 필요가 있다. DAMAGE 메시지에는 손상 정도를 가리키는 매개변수가 있다. 이것은 basicRobot 클래스에 있는 새로운 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 메시지는 충돌점수를 1점 공제하고 필요하면 세계에 KILLME 메시지를 보낸다.
WORLD 행위자가 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()라는 메쏘드를 도입한 것에 주목하자. 정상적으로 전송하는 대신에 이 메쏘드는 수신 분할작업에서 channel.receive()가 예외를 일으키도록 만든다. 여기에서는 스택리스 파이썬의 TaskletExit 예외를 일으키는데, 이 예외는 분할작업이 조용하게 종료하도록 만든다. 다른 예외도 일으킬 수 있지만, 임의의 예외가 제대로 처리되지 않으면, 메인 분할작업에서 다시 예외를 일으킨다.
이 프로그램의 완성된 버전은 아직 재미는 없지만, 실행해 보시면 위에 추가한 특징들이 모두 잘 작동하고 있음을 볼 수 있을 것이다. 로보트들은 충분히 충돌하고 나면 결국 죽어서 사라진다.
주의
이 섹션에서 완성된 프로그램은 actors3.py라는 이름으로 이 문서의 끝에 그 코드와 .zip 압축파일이 첨부되어 있다.
이제 시뮬레이션 메커니즘이 왼성되었으므로, 프로그램을 가지고 놀아 보면 된다. 무엇보다 먼저 다루어야 할 것은...
로보트가 죽을 때 그냥 사라지는 것은 별로 재미가 없다. 최소한 폭발은 해야 하겠다. 로보트는 죽을 때 폭발 행위자를 만들어낸다. 이것은 물리적이지 않다. 그래서 그냥 폭발 이미지를 보여준다. 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") )
이제 지뢰 매설 로보트를 만들어보자. 로보트 클래스보다 먼저 지뢰 클래스가 필요하다:
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점의 손상을 충돌 물체에 전송하고 자신은 사망한다.
mindropperRobot는 몇 가지 차이점만 제외하면 기본 로보트와 비슷하다. 먼저, 좀 다채롭게 하기 위해 minedropperRobot이 같은 방향으로 천천히 도는 대신에 구불구불하게 움직이도록 구성했다. 둘째, 지뢰 로보트는 매초 마다 지뢰를 만들어서 바로 자기 뒤에 매설한다:
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
산란장(Spawner pads)에서는 단순하게 무작위 속성과 위치로 매 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 # 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)
세계의 중심과 네 모서리에 산란장을 만들고 마무리를 짓겠다. 이제 필요한대로 새로 로보트를 만들어서 운영할 시뮬레이션을 확보했다. 자유롭게 새로운 로보트를 추가해서 시뮬레이션을 가지고 놀아보자.
적은 분량의 코드로 적당하게 복잡한 시뮬레이션을 만들어 내었다. 더 중요한 것은 각 행위자가 자율적으로 실행된다는 것이다. 우리가 건네는 메시지가 바로 API라고 생각하면 실제로 덧붙일 것이 별로 없다:
- WORLD_STATE
- JOIN
- UPDATE_VECTOR
- COLLISION
- KILLME
- DAMAGE
게다가, 행위자가 알아야할 것은 모두 자신 안에 캡슐화되어 있다. 외부 세계를 이해하기 위하여 이 여섯가지 메시지만 있으면, 프로그램도 단순해지고 우리도 그것을 간단하게 이해할 수 있다.
def ping():
print "PING"
pong()
def pong():
print "PONG"
ping()
ping()
#
# 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()
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)
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()
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 "\n\n\n"
if __name__ == "__main__":
run()
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 "\n\n\n"
if __name__ == "__main__":
run()
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)
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()
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()
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()