[Python-Dev] Change definition of Py_END_ALLOW_THREADS?

Mark Hammond mhammond@skippinet.com.au
Thu, 13 Feb 2003 10:37:24 +1100


> > > > However, to do this auto thread state work properly, I will
> > > > need to change the definition of Py_END_ALLOW_THREADS.
> > > > Specifically, Py_END_ALLOW_THREADS needs to be able to
> > > > handle the fact that the lock may or may not be held
> > > > when it is called (whereas now, the lock must *not* be held)
> > >
> > > Can you explain how the current thread, which explicitly
> > > released the
> > > lock in Py_BEGIN_ALLOW_THREADS, could end up owning the lock when
> > > Py_END_ALLOW_THREADS is entered?
> >
> > The short answer is that the current thread releases the lock (via
> > Py_BEGIN_ALLOW_THREAD), then makes the call it needs to.  This call
> > may synchronously call back into Python code, acquiring the lock.
> > Currently, the rule is that the callback must also release the lock
> > when done.  However, I am hoping to change things so that the
> > callback code need *not* re-release the lock when on the same
> > thread.
>
> Heh?  The point of Py_BEGIN_ALLOW_THREAD is to allow *other* threads
> to run, while the current thread engages in actions that could block
> indefinitely.  The code between BEGIN and END can cause any number of
> callbacks to run.  If one of those callbacks grabs the lock and
> doesn't release it, no other thread will be able to run, even if a
> later callback causes the current thread to block waiting for an
> external event!

This is true, but still true in many regards today.  Currently, between
BEGIN/END, a callback can still acquire the lock, leaving us in the same
position.  I am proposing that this lock need not be re-released.  In both
cases, there are periods of time where the current thread *does* hold the
lock between BEGIN/END pairs - I am simply increasing that time - but not to
the entire time.  Indeed, in the vast majority of cases where BEGIN/END is
currently used, there will be *no* change at all.

I agree I am on thin ice here, but let's move on for now.

> > The longer answer is to *why* I want to make that change:
> >
> > Consider:
> >
> > void SomeCallback()
> > {
> > 	PyAutoThreadState_Ensure();
> >       ... do some Python API stuff.
> >       SomeOtherFunc(); // my helper function.
> >       ... more Python stuff.
> >       PyAutoThreadState_Release();
> > }
> >
> > Now, consider SomeOtherFunc():
> >
> > void SomeOtherFunc()
> > {
> > 	// This function also needs to use the Python API
> > 	// but is called from many many places, where the
> > 	// Python context is not always known.
> > 	// So use the auto-thread-state
> > 	PyAutoThreadState_Ensure();
> > 	.. use the api
> > 	PyAutoThreadState_Release();
> > }
> >
> > As you can see, we have *nested* calls to
> PyAutoThreadState_Release().
> > The question is, in what state should PyAutoThreadState_Release()
> > leave Python?
>
> This suggests that the GIL needs to become a reentrant lock.

I don't think that solves our problem.

Consider a thread:

void SomeThreadEntryPoint()
  -> Acquire GIL
  -> Make some call into the Python API.
     -> Python code calls extension module fn.
        -> Extension module does Py_BEGIN_ALLOW_THREADS
...
  -> Release GIL

In this case, the Py_BEGIN_ALLOW_THREADS would release the GIL - but being a
reentrant lock, the lock itself would not actually be released, as
SomeThreadEntryPoint() has already acquired the same lock on the same
thread.

Thus, in this case, using a reentrant lock would *never* release the GIL in
Py_BEGIN_ALLOW_THREADS.  Indeed, from what I can see, the GIL will *never*
be released until SomeThreadEntryPoint terminates.  Current Python
thread-switch semantics assume that a GIL release is truly a lock release.

...
> Yes, releasing on the last call makes sense -- that's a
> reentrant lock.

Yes, but I am not sure a reentrant lock is suitable for the entire
thread-state API - I was hoping it would just be suitable for this
AutoThreadState API.  Now I am just confused :)

I need to ponder this some more, but it seems to me a reentrant lock is of
no use, as once a thread owns the lock, another thread can *never* acquire
it until the *final* matched release by the owning thread - and in some
cases that will be the entire lifetime of the thread.

Getting-harder-by-the-minute ly,

Mark.