Easy Concurrency with Stackless Python
Writing concurrent programs is hard. The concept is relatively straightforward; after all, we live concurrently. However, thinking concurrently in programming requires the programmer to anticipate lots of events that are never an issue in a single threaded, single process procedural program.
Python 2.5 provides two ways to write concurrent code: threads or forked processes. Both standard libraries have been available for years, and are stable and usable. That said, when interacting with the libraries, you have to pay a good amount of attention to detail, grabbing and releasing locks, communicating between threads, it can be a daunting task.
Stackless Python is an alternate distribution of Python that does away with the C call stack, and adds new "micro threading" libraries that provide a simpler interface to write threaded Python code.
The most vocal users of Stackless Python are the developers at CCP Games, makers of Eve Online, a massively multiplayer online spaceship combat game. In presentations at Pycon, they've attributed much of their scalability and developer agility to the speed and ease of use of Stackless. Their game cluster, written in Stackless Python, regularly handles 30,000+ users.
The Stackless concurrency model uses three concepts to provide its services, tasklets, a scheduler, and channels. You'll cover each concept one at a time, and then tie them together in a small example of bees looking for flowers.
Tasklets, the Building Blocks of Micro-Threads
Tasklets are the fundamental unit of work in the Stackless world. A tasklet is an object that knows how to pause and resume itself (using the scheduler), and can send and receive messages to other tasklets (using channels).
For your demo code, you'll start by writing a basic bee object that creates a tasklet for itself:
import stackless import random class Bee(object): """a bee, bees love nectar""" def __init__(self, id, flowers): self.nectar = 0 self.id = id self.flowers = flowers stackless.tasklet(self.find_nectar)() def find_nectar(self): """my brain""" while self.nectar < 5: flower = random.choice(flowers) flower.channel.send(self) print "Bee %s got nectar from Flower %s I now have %s nectar" % (self.id, flower.id, self.nectar)
The bee object takes an ID (a number) and a list of flowers as arguments. It creates a tasklet, passing its own find_nectar method as the tasklet runner. The tasklet is added to the stackless scheduler, and is started and stopped according to the scheduler.
The Scheduler, an Ever-Changing Work Queue
The standard Stackless scheduler is a round robin queue. When a tasklet is added to the queue, it's put on the bottom of the queue. When you call stackless.run() (which you'll do later on in the example), the queue is executed in order. As each tasklet is executed, it can add itself back to the queue at the end of the queue. Other actions the tasklet takes, such as listening on a communication channel, "block" the tasklet, and allow the next tasklet to begin working.
In this example, Bees are buzzing around, picking flowers at random, and drawing nectar from them. In the find_nectar method, the bee chooses a random flower, and then sends itself to the flower via a channel call.
Channels, Easy to Use Communication Between Tasklets
Channels are like walkie talkies between tasklets. They allow any object to be sent to a receiving tasklet. Create your Flower object, and give it a channel to receive communications from your bee.
class Flower(object): """a flower, has nectar""" def __init__(self, id): self.id = id self.channel = stackless.channel() stackless.tasklet(self.bee_receiver)() def bee_receiver(self): """receives a bee""" while True: print "flower %s is listening" % self.id bee = self.channel.receive() bee.nectar = bee.nectar + 1 print "Flower %s is giving Bee %s nectar" % (self.id, bee.id)
The flower class is relatively similar to the bee class. In the __init__ block, you add a channel listener, and then tell the scheduler to add the method 'bee_receiver' to the scheduler. 'bee_receiver' immediately blocks, waiting for a channel message from a bee. Because the bee sends itself as the message, you can access the bee's methods and properties directly. The flower adds a nectar to the bee, and then prints out some diagnostic information. The loop continues, and then blocks while waiting for another message from a bee.
Finally, you need to initialize your objects, and get them to start talking to one another:
# let's start the world flowers =  # add 5 flowers for id in range(5): flowers.append(Flower(id)) # add 5 bees for id in range(5): bee = Bee(id, flowers) stackless.run()