global interpreter lock not working as it should

Armin Steinhoff a-steinhoff at web.de
Fri Aug 2 17:05:38 CEST 2002


anton wilson <anton.wilson at camotion.com> wrote in message 
[ clip ..]
> 
> Maybe I am not being clear enough. I am concerned with a multi-threaded 
> program that does not do any form of blocking on a Linux/Unix box. I DO 
> expect a thread to block on the GIL every 10 byte codes. However, I have 
> proved with my results that this does NOT happen. Any thread that is 
> completely CPU bound will never give up the CPU for as long as 
> 1) it can run
> 2) it has work to do
> 
> I have proven this by even examining what happens within the interpreter 
> with 
this code in ceval:
> 
>                                 oldt = tstate;    /*<---- My code*/
> 
>                                 if (PyThreadState Swap(NULL) != tstate)
>                                   Py FatalError("ceval: tstate mix-
up");
>                                 PyThread release lock(interpreter lock);
> 
>                                 /* Other threads may run now */
>                                 /*sched yield();*/

I see your point. The release_lock / acquire_lock make only sense if
the threads have different priorities. But all Python threads have by
default the same priority ... so it makes absolutely sense to include
the sched_yield for a fair scheduling.

Without the sched_yield ... a Python thread can catch the CPU for
ever. (if not preempted by a task with a higher prio)

Well .. the threads don't care about the comment 
         /* other threads may run now */
in eval.c  :-)

Armin


> 
>                                 PyThread acquire lock(interpreter lock, 1
> );
> 
>                                 if (PyThreadState Swap(tstate) != NULL)
>                                         Py FatalError("ceval: orphan tsta
> te");
> 
>                                 if(tstate == oldt)    /*< ------- my 
> code*/  
>                                    printf("bad things have happened\n");
> 
> 
> The great majority of the time, my print statement will be printed, meani
> ng 
> the GIL was not released.
> 
>  but the put-it-up-for-grabs-every-10-instructions functionality
> > works just fine too. Consider:
> >
> > import threading, time
> >
> > COUNT = 3
> > counters = [0] * COUNT
> >
> > def Worker(i):
> >     while 1:
> >         counters[i] += 1
> >
> > for i in range(COUNT):
> >     threading.Thread(target=Worker, args=(i,)).start()
> >
> > while 1:
> >     time.sleep(1.0)
> >     print counters
> >
> > Here's some output:
> > [162565, 176016, 165796]
> > [329009, 327856, 333183]
> > [497881, 496857, 498133]
> > [665567, 679094, 643678]
> > [810255, 845521, 811988]
> > [968056, 1008142, 974790]
> >
> > Lo and behold, each thread is getting execution time, and nearly equal
> > execution time at that!
> 
> 
> There are several reasons why your program seems to work.
> The first obvious reason is that the main thread sleeps. If you remove th
> e 
> sleep, you will see output that looks like this
> 
> [0, 0, 0]
> [0, 0, 0]
> [0, 0, 0]
> [0, 0, 0]
> [0, 0, 0]
> [0, 0, 0]
> [0, 0, 0]
> [0, 0, 0]
> [0, 0, 0]
> 
> ....(100+ times in all)...
> 
> [35499, 16419, 0]
> [35499, 16419, 0]
> [35499, 16419, 0]
> [35499, 16419, 0]
> [35499, 16419, 0]
> [35499, 16419, 0]
> [35499, 16419, 0]
> [35499, 16419, 0]
> [35499, 16419, 0]
> 
> 
> ...(100+ times in all) ....
> 
> [35499, 16419, 11556]
> [35499, 16419, 11556]
> [35499, 16419, 11556]
> [35499, 16419, 11556]
> [35499, 16419, 11556]
> [35499, 16419, 11556]
> [35499, 16419, 11556]
> 
> 
> .....etc ....
> 
> 
> This proves that the GIL does not block very often, and definitely not ev
> ery 
> 10 byte codes. Think about this for a while.
> 
> I made a python interpreter with a sched yield inbetween the acquire and 
> release calls and my results looked like this with your sleep removed:
> 
> [1886, 1887, 0]
> [1886, 1887, 0]
> [1886, 1888, 0]
> [1886, 1889, 0]
> [1886, 1890, 0]
> [1886, 1890, 0]
> [1886, 1890, 0]
> [1886, 1891, 0]
> [1886, 1892, 0]
> [1886, 1893, 0]
> [1886, 1893, 0]
> 
> .....etc.......
> 
> Here, you will notice that there are constant changes. The GIL releasing 
> is 
> working as intended.
> 
> 
> This brings me to the second reason that your program seems to work.
> The Linux OS gives threads time-slices and when these time-slices are use
> d up 
> every 150 or so milliseconds, the process is forcibly removed from the CP
> U.
> I presume that the reason your program seems to work is that in the time 
> between when a thread releases the GIL and a thread tries to reaquire the
>  
> GIL, it is forcibly removed from the CPU, and the other thread can now ru
> n. 
> This would not be a rare occurence due to the high frequency at which the
>  
> lock is released.
> 
> To prove this, I ran the program using sched rr threads and changed the 
> kernel so that round robin threads had no timeslice. In this case I saw t
> his 
> output once per second:
> 
> [0, 0, 0]
> [0, 586126, 0]
> [0, 1194859, 0]
> [0, 1802596, 0]
> [0, 2414027, 0]
> 
> Because the thread is never forced by the OS to relinquish the CPU, the 
> thread will never ever lose the GIL. If the GIL was actually working core
> ctly 
> and blocking, the second thread would not retain the CPU past 10 byte cod
> es.
> 
> 
> So, the GIL does not blcok as intended, and this probably needs to be loo
> ked 
> into.
> 
> Anton



More information about the Python-list mailing list