Ian Bicking: the old part of his blog

Comment

In static languages like C, an event-based system offers some complication and hindrance. But in Python, it has been relatively easy for me to write in an event-based manner, because of dynamic function declaration.

Here's a code snippet from my application, Mnet, boiled down to minimal form:

def request_block_from_peer(self, peerId, blockId):
def _handle_request_block_result(outcome, failure_reason=None):
# No matter what comes out of this reply, we are removing this
# blockId/peerId pair from the located-blocks data. (Because either
# this result means that we now have the block, or it means that the
# peer does not have the block!)
self.data.remove_Ids_of_located_blocks(peerId, blockIds=(blockId,))
if failure_reason or (outcome['result'] != "success"):
# Inform reliability handicapper that this was actually a failure.
self.mtm._keeper.get_counterparty_object(peerId).decrement_reliability()
# Wake the block wrangling strategy -- failure to retrieve a block is the kind of thing that it might want to respond to.
self.strategy.schedule_event()
else:
self._handle_block(blockId, outcome['data'], peerId)

self.mtm.initiate(peerId, "request block", blockId, outcome_func=_handle_timely_request_block_result)

Back in the bad old days when this project used Python's multithreading, this code would have looked something like this:

def request_block_from_peer(self, peerId, blockId):
try:
outcome = self.mtm.initiate(peerId, "request block", blockId)
except TransactionFailure, le:
outcome = {'result': "transaction failure", 'le': le}

# No matter what comes out of this reply, we are removing this
# blockId/peerId pair from the located-blocks data. (Because either
# this result means that we now have the block, or it means that the
# peer does not have the block!)
self.data.remove_Ids_of_located_blocks(peerId, blockIds=(blockId,))

if outcome['result'] != "success":
# Inform reliability handicapper that this was actually a failure.
self.mtm._keeper.get_counterparty_object(peerId).decrement_reliability()
# Wake the block wrangling strategy -- failure to retrieve a block is the kind of thing that it might want to respond to.
self.strategy.notify()
return None
else:
return (blockId, outcome['data'], peerId,)


Now, the multithreading version might be easier to read because the sequence of runtime events follows the sequence of code, but I hope you will agree that the event-based version is clear enough.

The nice thing about the event-based approach is that I don't have to spend any time worrying about the possibility that data will change out from under me during the execution of the "_handle_request_block_result()" function. That function, and *all* functions, get executed atomically, in their entirety, without any of the data that they depend on changing in any way other than as dictated by the code of that one function. It is difficult to express what a blessed relief it is to be able to just naively read the code and assume that if it says "remove the id and then decrement the reliability" then both of those effects will happen atomically, and it will not make any difference which order you do them in, and the data will not unexpectedly change between the first event and the second.

Anyway, when I started out to write this note I just wanted to emphasize to you that for me, using Python, switching from multithreaded to event-based was relatively painless. I've also had to do the same in some C code recently and it was a lot more hassle in C.
Comment on Concurrency: looking for positive models
by Zooko