Python vs. Ruby: threads - advice appreciated

Tim Peters tim_one at email.msn.com
Mon May 29 16:10:40 EDT 2000


[posted & mailed]

[Jean-Paul Smets]
> I am currently comparing Python and Ruby for a personal project
> which requires many many threads.
>
> I came across the "philosophers" example in Ruby and was surprised by its
> compacity (see below).
> I am wondering if it would be easy to write such an example in
> Python with as few lines of code ?

Fewer (Python doesn't require the noise "end" lines), but I won't show
that -- approaching a large-thread project with masses of global vrbls and
repeated inline fiddly arithmetic (to map integer ids to conceptual objects
to conceptual properties of those objects via global arrays) is a dead end
for maintainability.  Instead I'll attach a Python example that uses its
higher-level objected-oriented thread interface:  treat a Philospher as an
object, and then, e.g., it's natural and easy to give each Philospher named
left_fork and right_fork objects, greatly increasing the readability of the
code after initial setup.

> Also, I tried to push Ruby to its limits (N=10000) and everything
> went OK (50 Mo RAM was occupied though but this is only 5Ko per
> thread which I find quite good).

This almost certainly has much more to do with the operating system you're
running on than with the language you use.  A Python thread requires trivial
overhead beyond what the underlying platform thread requires; don't know
about Ruby, but suspect it's much the same there (if a language supplies a
native thread, it can't require *less* burden than a platform thread
demands!).  Note that if your 5Kb/thread is accurate, and it includes space
for the thread stack, your platform's threads are going to blow up if a
thread does deep function calls (indeed, on most platforms it's a thread's
stack that accounts for the bulk of a thread's memory burden).

> I wonder if it is possible to go that far with Python, either with
> native threads

You didn't name your OS, so nobody can try a comparable experiment.  The
Python below invites 1,000 Philosophers to dinner, and ran fine without
thrashing on an old 32Mb Win95 box.  I know from experience I can't push
this particular box much farther than that.

> or with microthreads in stackless Python ?

A Stackless microthread has much less overhead than a native thread (largely
because it doesn't have to pre-reserve space for "a stack"), but note that
Stackless is not part of the std Python distribution.

> Finaly, I found that Ruby has a very nice and completely
> transparent interface to Python libraries and extensions. So,
> I am wondering now what would be the inconvenience for me of
> switching to Ruby for my project.

Beats me.  Even if I understood the Ruby code, I would find the Python below
much more readable, and for a large project that's of overwhelming
importance to me.  I don't know whether Ruby can be made comparably easy to
live with, but suspect it probably can be.

[the Ruby example]
> #
> # The Dining Philosophers - thread example
> #
> require "thread"
>
> srand
> #srand
> N=10				# number of philosophers
> $forks = []
> for i in 0..N-1
>   $forks[i] = Mutex.new
> end
> $state = "-o"*N
>
> def wait
>   sleep rand(20)/10.0
> end

I personally hate this Perl-like "function calls don't require parens"
gimmick.  In Perl it can kill you.  In Ruby, I don't know whether to parse
that line as

    sleep(rand(20)/10.0)

or

    sleep(rand(20))/10.0

although from *context* only the first makes any sense.

> def think(n)
>   wait
> end
>
> def eat(n)
>   wait
> end
>
> def philosopher(n)
>   while TRUE
>     think n
>     $forks[n].lock
>     if not $forks[(n+1)%N].try_lock
>       $forks[n].unlock		# avoid deadlock
>       next
>     end
>     $state[n*2] = ?|;
>     $state[(n+1)%N*2] = ?|;
>     $state[n*2+1] = ?*;
>     print $state, "\n"

Surely this is buggy:  unless Ruby has some implicit global function lock
that serializes calls to philosopher, multiple threads can be mucking with
the global $state string simultaneously, and so the output can be
inconsistent.

>     eat(n)
>     $state[n*2] = ?-;
>     $state[(n+1)%N*2] = ?-;
>     $state[n*2+1] = ?o;
>     print $state, "\n"

Ditto.

>     $forks[n].unlock
>     $forks[(n+1)%N].unlock
>   end
> end
>
> for i in 0..N-1
>   Thread.start{philosopher(i)}
>   sleep 0.1
> end

What purpose does that sleep serve?

> sleep

Ditto.

overkill-attached-ly y'rs  - tim

import threading, random, time

# Number of philosophers.
N = 1000

# Number of times a philosopher needs to eat.
# Letting the program run forever is cute, but there isn't
# that much food in the universe <wink>.
EATME = 10

def wait(atmost=2):
    """Wait a random time, by default at most 2 seconds."""
    time.sleep(random.random() * atmost)

# Exception raised to let a philosopher know that she can't
# get both forks.
class ForksBusy(Exception):
    pass

class Philosopher(threading.Thread):
    def __init__(self, id):
        threading.Thread.__init__(self)
        self.id = id
        self.left_fork = forks[id]
        self.right_fork = forks[(id + 1) % N]
        self.feedings = 0

    def run(self):
        # Compare this main loop:  using OO organization, the
        # logic is much less cluttered, and so correspondingly
        # easier to understand and extend.
        while self.feedings < EATME:
            try:
                self.think()
                self.acquire_forks()  # may raise ForksBusy
                self.eat()
                self.release_forks()
            except ForksBusy:
                pass
        print "philosopher %d stuffed" % self.id

    def think(self):
        print "philosopher %d thinking" % self.id
        wait()

    def eat(self):
        self.feedings = self.feedings + 1
        print "philosopher %d on feeding #%d" % (self.id,
                                                 self.feedings)
        wait()

    def acquire_forks(self):
        self.left_fork.acquire()
        if not self.right_fork.acquire(0):
            self.left_fork.release()
            raise ForksBusy

    def release_forks(self):
        self.left_fork.release()
        self.right_fork.release()

# Create the forks.
forks = []
for i in range(N):
    forks.append(threading.Lock())

# Create the philosophers, and start their contemplative lives.
philosophers = []
for i in range(N):
    p = Philosopher(i)
    philosophers.append(p)
    p.start()

# Wait for everyone to finish.
for p in philosophers:
    p.join()

print "Dinner is complete."






More information about the Python-list mailing list