Threading unfairness

Tim Peters tim.one at comcast.net
Sat May 18 21:08:55 EDT 2002


[Matt Kimball]
> While writing some multithreaded GUI code with ActivePython 2.2.1
> under Windows XP, on a single processor machine, and I noticed that
> my user-interface thread was very unresponsive when I had a background
> computation thread going.  This annoyed me.  It seemed like the main
> thread where the GUI code was running was being starved of CPU time.

It can be very difficult to sort things like that out.

> Since there doesn't seem to be any way to set thread priorities
> with Python,

There is not.  And it wouldn't help if there were <0.5 wink>.

> I "fixed" the problem with inserting frequent 'time.sleep(0)' calls
> in my compute-intensive thread, and that seemed to give my main GUI
> thread more time to execute when a user-interface event occurred.

That works on Windows because the underlying Win32 API Sleep() call takes an
argument of 0 as *meaning* "yield the timeslice".  On other OSes it may or
may not work.  For example, people playing this trick on Solaris usually use
time.sleep(0.01) instead, because time.sleep(0) is a nop there.

> However, it got me wondering just how bad the situation was, so I
> wrote a simple program to test Python threads, and find the maximum
> time that individual threads go without getting any CPU time.
> Interestingly, the problem seems worse with a low number of threads.
> ...
> The code I used to measure this is at
> http://matt.kimball.net/test_thread.py --
> Maybe someone wants to look it over to make sure I'm not doing
> anything funny, and run a few tests on other operating systems to see
> how they differ.

Sorry, I had too much trouble understanding the code, so wrote a different
driver that seems to me much simpler and more informative.  See attached.
Here's a typical run on Win98SE, after killing most background processes
(note:  that's important -- Python isn't the OS, and has no control over
when the OS decides to boot your program to run some other task):

With span 1.0 and 1 threads:
tid   0 meangap   0.01 maxgap  47.98 ms at iter   2313 of 103305

With span 1.0 and 2 threads:
tid   0 meangap   0.04 maxgap  12.41 ms at iter  22985 of  22985
tid   1 meangap   0.05 maxgap  15.06 ms at iter  18494 of  22079

With span 1.0 and 3 threads:
tid   0 meangap   0.07 maxgap   0.73 ms at iter    517 of  14530
tid   1 meangap   0.07 maxgap   1.02 ms at iter  14018 of  14101
tid   2 meangap   0.07 maxgap   1.02 ms at iter  14013 of  14107

With span 1.0 and 4 threads:
tid   0 meangap   0.09 maxgap   1.06 ms at iter    298 of  10765
tid   1 meangap   0.10 maxgap   1.04 ms at iter  10475 of  10506
tid   2 meangap   0.10 maxgap   1.05 ms at iter  10469 of  10513
tid   3 meangap   0.10 maxgap   0.94 ms at iter  10461 of  10509

With span 1.0 and 5 threads:
tid   0 meangap   0.11 maxgap   1.08 ms at iter   2550 of   8853
tid   1 meangap   0.12 maxgap   1.11 ms at iter   4399 of   8422
tid   2 meangap   0.12 maxgap   1.08 ms at iter   2074 of   8425
tid   3 meangap   0.12 maxgap   1.11 ms at iter   8385 of   8424
tid   4 meangap   0.12 maxgap   1.00 ms at iter   8379 of   8425

With span 1.0 and 6 threads:
tid   0 meangap   0.13 maxgap   1.39 ms at iter   5312 of   7646
tid   1 meangap   0.14 maxgap   1.50 ms at iter   6894 of   6970
tid   2 meangap   0.14 maxgap   1.31 ms at iter    746 of   6972
tid   3 meangap   0.14 maxgap   1.53 ms at iter   6883 of   6972
tid   4 meangap   0.14 maxgap   1.31 ms at iter   6509 of   6971
tid   5 meangap   0.14 maxgap   1.39 ms at iter    729 of   6971

With span 1.0 and 7 threads:
tid   0 meangap   0.16 maxgap   1.61 ms at iter   1396 of   6213
tid   1 meangap   0.17 maxgap   1.56 ms at iter   2792 of   5981
tid   2 meangap   0.17 maxgap   1.60 ms at iter   2786 of   5982
tid   3 meangap   0.17 maxgap   1.52 ms at iter   1136 of   5982
tid   4 meangap   0.17 maxgap   1.66 ms at iter   1130 of   5981
tid   5 meangap   0.17 maxgap   1.57 ms at iter   1126 of   5980
tid   6 meangap   0.17 maxgap   1.56 ms at iter   2765 of   5978

Note the gap times are measured in milliseconds here, not seconds.  The
worst gap was less than 50ms, which isn't noticeable to most people.  Note
that the iterations at which the max gaps occur tend to cluster:  this is
evidence that *all* threads in the program are getting booted, due to the OS
giving some other process a chance to run.

WRT other platforms:

1. This will probably print gaps of 0.  time.clock() has extraordinarily
   fine resolution on Windows (finer than microsecond).  On other
   platforms, change the "now" import to use "time" instead of "clock".

2. Of all the OSes I've futzed with, Windows flavors are actually the
   best-behaved in this sense wrt threads:  Windows is much happier to
   switch threads frequently than other OSes.  Since typical Windows apps
   are thread-happy GUI programs, this probably isn't an accident.

> I think this behavior is probably highly operating system
> dependent.

Extremely, yes.

> The python interpreter is assuming the OS will do something reasonably
> fair when it gives up the global interpreter lock,

Python isn't assuming anything here.  "It's a feature" that Python thread
behavior reflects your native thread behavior; Python isn't trying to be an
operating system.

> ...
> Still, it would be nice if something could be done to make
> threading execution more fair,

As the output above shows, scheduling is very fair on Win98SE, except for
slightly favoring the first thread spawned (tid 0 always got to do more
iterations of its loop than the other tids, while the number of iterations
the other tids got to do are amazingly close to each other).

> but I'm not sure how to do this without creating some sort of
> scheduler in the python interpreter itself.

It wouldn't work -- Python can't tell the OS kernel how to schedule.  You
may be able to *influence* that via piles of platform-specific extension
modules.  BTW, XP may have a slider or swtich buried in one of the Control
Panel applets to influence whether foreground processes get preference in
scheduling (I can't remember, and don't have XP here now).

> ...
> Is the problem as bad under other operating systems?

Probably worse.

> Am I insane to try to do anything in the neighborhood of
> soft-realtime with Python?

Start with a soft-realtime OS.  After spending a few miserable weeks on it,
you'll probably find that the apparent unfairness in your GUI app is a
shallow problem after all <0.9 wink>.

threads-are-a-hoot-ly y'rs  - tim

import threading
from time import clock as now

SPAN = 1.0
MAXTHREADS = 100

maxgap  = [None] * MAXTHREADS
meangap = [None] * MAXTHREADS
maxiter = [None] * MAXTHREADS
totalit = [None] * MAXTHREADS

def busy_thread(index):
    maxgap[index] = -1.0
    current = last = now()
    finish = current + SPAN
    i = 0
    totalgaps = 0.0
    while current <= finish:
        current = now()
        i += 1
        delta = current - last
        totalgaps += delta
        if delta > maxgap[index]:
            maxgap[index] = delta
            maxiter[index] = i
        last = current
    totalit[index] = i
    meangap[index] = totalgaps / i

def drive(nthreads):
    threads = [threading.Thread(target=busy_thread, args=(i,))
               for i in range(nthreads)]

    for t in threads:
        t.start()
    for t in threads:
        t.join()

    print "With span", SPAN, "and", nthreads, "threads:"
    for i in range(nthreads):
        print ("tid %3d meangap %6.2f maxgap %6.2f ms "
               " at iter %6d of %6d" % (i, meangap[i] * 1000,
               maxgap[i] * 1000, maxiter[i], totalit[i]))
    print

for i in range(1, 8):
    drive(i)






More information about the Python-list mailing list