Thread scheduling

Peter Hansen peter at engcorp.com
Sat Feb 26 19:08:45 EST 2005


Jack Orenstein wrote:
> I am using Python 2.2.2 on RH9, and just starting to work with Python
> threads.

Is this also the first time you've worked with threads in general,
or do you have much experience with them in other situations?

> This program seems to point to problems in Python thread scheduling.

While from time to time bugs in Python are found, it's generally
more productive to suspect one's own code.  In any case, you
wouldn't have posted it here if you didn't suspect your own
code at least a bit, so kudos to you for that. :-)

> done = 0
> def run(id):
>     global done
>     print 'thread %d: started' % id
>     global counter
>     for i in range(nCycles):
>         counter += 1
>         if i % 10000 == 0:
>             print 'thread %d: i = %d, counter = %d' % (id, i, counter)
>     print 'thread %d: leaving' % id
>     done += 1
> 
> for i in range(nThreads):
>     thread.start_new_thread(run, (i + 1,))
> while done < nThreads:
>     time.sleep(1)
>     print 'Still waiting, done = %d' % done
> print 'All threads have finished, counter = %d' % counter

Without having tried to run your code, and without having studied
it for long, I am going to point out something that is at the
very least an inherent defect in your code, though you might
not have used Python (or threads?) for long enough to realize
why.  Note that I don't know if this is the cause of your
particular problem, just that it _is_ a bug.

You've got two shared global variables, "done" and "counter".
Each of these is modified in a manner that is not thread-safe.
I don't know if "counter" is causing trouble, but it seems
likely that "done" is.

Basically, the statement "done += 1" is equivalent to the
statement "done = done + 1" which, in Python or most other
languages is not thread-safe.  The "done + 1" part is
evaluated separately from the assignment, so it's possible
that two threads will be executing the "done + 1" part
at the same time and that the following assignment of
one thread will be overwritten immediately by the assignment
in the next thread, but with a value that is now one less
than what you really wanted.

Look at the bytecode produced by the statement "done += 1":

 >>> import dis
 >>> def f():
...   global done
...   done += 1
...
 >>> dis.dis(f)
   3           0 LOAD_GLOBAL              0 (done)
               3 LOAD_CONST               1 (1)
               6 INPLACE_ADD
               7 STORE_GLOBAL             0 (done)
(ignore the last two lines: they just "return None")
              10 LOAD_CONST               0 (None)
              13 RETURN_VALUE

Note here the "store_global" that is separate from the add
operation itself.  If thread A gets loses the CPU (so to speak)
just before that operation, and thread B executes the entire
suite of operations, then later on when thread A executes
that operation it will effectively result in only a single
addition operation being performed, not two of them.

If you really want to increment globals from the thread, you
should look into locks.  Using the "threading" module (as is
generally recommended, instead of using "thread"), you would
use threading.Lock().  There are other thread synchronization
primitives in the threading module as well, some of which
might be more suitable for your purposes.  Note also the
oft-repeated (in this forum) recommendation that if you simply
use nothing but the Queue module for inter-thread communication,
you will be very unlikely to stumble over such issues.

-Peter



More information about the Python-list mailing list