Ian Bicking: the old part of his blog

Concurrency: looking for positive models

My recent post on Prothon got several comments about threading. Which is odd, since I hadn't mentioned threading, and it really was entirely off-topic. (OK, technically it related to Prothon's lack of a Global Interpreter Lock, but I ignored that in my post because the GIL bores me)

During PyCon Bruce Eckel talked a little bit about threads in his speach. Not a lot -- it was incidental to the main topic. But it got a strong reaction from people -- heckling from the crowd (good spirited though it was), and I'm sure Bruce heard other reactions after. Whenever someone asks how to use threads, there's always someone else who says "don't".

Personally, my experience hasn't been so bad. There's been some annoying problems, but frankly concurrency is hard, and a lot of the problems have been due to concurrency issues that wouldn't be magically solved by a different form of concurrency. And a lot of people get things done with threads... isn't that the real test?

What I don't see is a practical alternative being presented in most cases. That is, an alternative that is easy to use, reliable, and doesn't require you to twist your mind around new ways of programming (pun not intended... or maybe it is subconsciously?)

When people are getting stuff done using threads, it's not a very convincing argument to tell them they are just wrong. And you can take my blocking calls from my cold, dead hands! So what's the realistic alternative? What's the example application that deals with concurrency safely and pleasantly, with a manageable codebase that is friendly to outsiders? I don't want to drink the Kool Aid, I just want to get things done. For all those that feel the same, threads so far have been the safe bet.

(Okay, I'll admit it, I wrote this while procrastinating on fixing some threading bugs; oh irony!)

Created 30 Mar '04
Modified 14 Dec '04

Comments:

How about [1]medusa that uses multiplexing instead of multi-process or multithread?

1. http://www.amk.ca/python/code/medusa.html
# pm5

Message Passing Concurrency(aka CSP) and Declarative Concurrency.

I know of some great results using CSP in real-world large projects.
# Two saviors to threading

The paper Why events are a bad idea throughs some light on this issue.
# Juan M. Bello Rivas

Reading through this and the previous discussions, Jython seems to provide an MP solution for those who need it. IronPython might as well, so why throw out the baby (python) or the bathwater (GIL)?
In comparative ignorance
# paul

The "Why events are a bad idea" paper DOES NOT APPLY TO PRE-EMPTIVE THREADING IMPLEMENTATIONS LIKE PTHREADS! Yes, of course, if you write your own specific threading implementation designed to be faster, easier and more predictable than standard works-in-my-compiler-right-now events, then YES IT WILL BE EASIER. No shit!

If you want "blocking" calls without koolaid, use Stackless. It offers a CSP paradigm. Otherwise, you're going to need to drink some koolaid because Python needs macros and/or new syntax to do other kinds of "sugar free" magic concurrency.
# Bob Ippolito

One word: Oz!

Oz isn't the last (or the first) word in declarative concurrency, but I strongly recommend playing with it, if only for a little while, just so you can see how much the DC/CSP approach changes things. It doesn't solve all your problems at once, but it gives you a perspective from which the "low-hanging fruit" appears in *completely* different places.
# Dominic Fox

# Al

Yeah, I would've much rather seen a PythOz than a Prothon :) I like Oz's feature set, but not its syntax.
# Bob Ippolito

I agree with you Ian. Until I'm dragged into some environment that provides a relatively easy alternative to threads (by which I mean, I can do blocking calls or something that looks a lot like them), I'm sticking with threads. A lot of people say that "threads are for people who don't understand a state machine". I've written my share of event-driven asynchronous code in which I need to maintain a state machine, and I'll just need to admit that, yes, I'm too dumb. The faster I need to get the job done, the dumber I become. It just takes me far too long to figure out what the states *are*, especially in anything more than something trivial. time.sleep(30)! ;-)
# Chris McDonough

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.
# Zooko

Hm. Here are the two code snippets with "_" in place of " ". Sorry about that.

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.

# Zooko

I'm coming into this late and with only a partial comment, but another approach is coroutines (a special case of continuations). Coroutines feel like threads but are non-preemptive like events. Python generators are themselves a special case of coroutines and Stackless Python has [had?] coroutines and continuations (although theirs seems a little more obtuse than I think it should be).

As an example application, SimPy uses generators in a "coroutine like" fashion for creating dozens or hundreds of "threads of execution". The code for the each "thread" is straightforward and linear, normal Python methods.
# Ken MacLeod