mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 21:51:50 +00:00 
			
		
		
		
	gh-118727: Don't drop the GIL in drop_gil() unless the current thread holds it (#118745)
				
					
				
			`drop_gil()` assumes that its caller is attached, which means that the current thread holds the GIL if and only if the GIL is enabled, and the enabled-state of the GIL won't change. This isn't true, though, because `detach_thread()` calls `_PyEval_ReleaseLock()` after detaching and `_PyThreadState_DeleteCurrent()` calls it after removing the current thread from consideration for stop-the-world requests (effectively detaching it). Fix this by remembering whether or not a thread acquired the GIL when it last attached, in `PyThreadState._status.holds_gil`, and check this in `drop_gil()` instead of `gil->enabled`. This fixes a crash in `test_multiprocessing_pool_circular_import()`, so I've reenabled it.
This commit is contained in:
		
							parent
							
								
									b30d30c747
								
							
						
					
					
						commit
						be1dfccdf2
					
				
					 5 changed files with 73 additions and 60 deletions
				
			
		|  | @ -83,6 +83,8 @@ struct _ts { | ||||||
|         unsigned int bound_gilstate:1; |         unsigned int bound_gilstate:1; | ||||||
|         /* Currently in use (maybe holds the GIL). */ |         /* Currently in use (maybe holds the GIL). */ | ||||||
|         unsigned int active:1; |         unsigned int active:1; | ||||||
|  |         /* Currently holds the GIL. */ | ||||||
|  |         unsigned int holds_gil:1; | ||||||
| 
 | 
 | ||||||
|         /* various stages of finalization */ |         /* various stages of finalization */ | ||||||
|         unsigned int finalizing:1; |         unsigned int finalizing:1; | ||||||
|  | @ -90,7 +92,7 @@ struct _ts { | ||||||
|         unsigned int finalized:1; |         unsigned int finalized:1; | ||||||
| 
 | 
 | ||||||
|         /* padding to align to 4 bytes */ |         /* padding to align to 4 bytes */ | ||||||
|         unsigned int :24; |         unsigned int :23; | ||||||
|     } _status; |     } _status; | ||||||
| #ifdef Py_BUILD_CORE | #ifdef Py_BUILD_CORE | ||||||
| #  define _PyThreadState_WHENCE_NOTSET -1 | #  define _PyThreadState_WHENCE_NOTSET -1 | ||||||
|  |  | ||||||
|  | @ -131,11 +131,10 @@ extern int _PyEval_ThreadsInitialized(void); | ||||||
| extern void _PyEval_InitGIL(PyThreadState *tstate, int own_gil); | extern void _PyEval_InitGIL(PyThreadState *tstate, int own_gil); | ||||||
| extern void _PyEval_FiniGIL(PyInterpreterState *interp); | extern void _PyEval_FiniGIL(PyInterpreterState *interp); | ||||||
| 
 | 
 | ||||||
| // Acquire the GIL and return 1. In free-threaded builds, this function may
 | extern void _PyEval_AcquireLock(PyThreadState *tstate); | ||||||
| // return 0 to indicate that the GIL was disabled and therefore not acquired.
 |  | ||||||
| extern int _PyEval_AcquireLock(PyThreadState *tstate); |  | ||||||
| 
 | 
 | ||||||
| extern void _PyEval_ReleaseLock(PyInterpreterState *, PyThreadState *); | extern void _PyEval_ReleaseLock(PyInterpreterState *, PyThreadState *, | ||||||
|  |                                 int final_release); | ||||||
| 
 | 
 | ||||||
| #ifdef Py_GIL_DISABLED | #ifdef Py_GIL_DISABLED | ||||||
| // Returns 0 or 1 if the GIL for the given thread's interpreter is disabled or
 | // Returns 0 or 1 if the GIL for the given thread's interpreter is disabled or
 | ||||||
|  |  | ||||||
|  | @ -17,7 +17,7 @@ | ||||||
| from test.support import verbose | from test.support import verbose | ||||||
| from test.support.import_helper import forget, mock_register_at_fork | from test.support.import_helper import forget, mock_register_at_fork | ||||||
| from test.support.os_helper import (TESTFN, unlink, rmtree) | from test.support.os_helper import (TESTFN, unlink, rmtree) | ||||||
| from test.support import script_helper, threading_helper, requires_gil_enabled | from test.support import script_helper, threading_helper | ||||||
| 
 | 
 | ||||||
| threading_helper.requires_working_threading(module=True) | threading_helper.requires_working_threading(module=True) | ||||||
| 
 | 
 | ||||||
|  | @ -248,9 +248,6 @@ def test_concurrent_futures_circular_import(self): | ||||||
|                           'partial', 'cfimport.py') |                           'partial', 'cfimport.py') | ||||||
|         script_helper.assert_python_ok(fn) |         script_helper.assert_python_ok(fn) | ||||||
| 
 | 
 | ||||||
|     # gh-118727 and gh-118729: pool_in_threads.py may crash in free-threaded |  | ||||||
|     # builds, which can hang the Tsan test so temporarily skip it for now. |  | ||||||
|     @requires_gil_enabled("gh-118727: test may crash in free-threaded builds") |  | ||||||
|     def test_multiprocessing_pool_circular_import(self): |     def test_multiprocessing_pool_circular_import(self): | ||||||
|         # Regression test for bpo-41567 |         # Regression test for bpo-41567 | ||||||
|         fn = os.path.join(os.path.dirname(__file__), |         fn = os.path.join(os.path.dirname(__file__), | ||||||
|  |  | ||||||
|  | @ -205,32 +205,39 @@ static void recreate_gil(struct _gil_runtime_state *gil) | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
| static void | static inline void | ||||||
| drop_gil_impl(struct _gil_runtime_state *gil) | drop_gil_impl(PyThreadState *tstate, struct _gil_runtime_state *gil) | ||||||
| { | { | ||||||
|     MUTEX_LOCK(gil->mutex); |     MUTEX_LOCK(gil->mutex); | ||||||
|     _Py_ANNOTATE_RWLOCK_RELEASED(&gil->locked, /*is_write=*/1); |     _Py_ANNOTATE_RWLOCK_RELEASED(&gil->locked, /*is_write=*/1); | ||||||
|     _Py_atomic_store_int_relaxed(&gil->locked, 0); |     _Py_atomic_store_int_relaxed(&gil->locked, 0); | ||||||
|  |     if (tstate != NULL) { | ||||||
|  |         tstate->_status.holds_gil = 0; | ||||||
|  |     } | ||||||
|     COND_SIGNAL(gil->cond); |     COND_SIGNAL(gil->cond); | ||||||
|     MUTEX_UNLOCK(gil->mutex); |     MUTEX_UNLOCK(gil->mutex); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void | static void | ||||||
| drop_gil(PyInterpreterState *interp, PyThreadState *tstate) | drop_gil(PyInterpreterState *interp, PyThreadState *tstate, int final_release) | ||||||
| { | { | ||||||
|     struct _ceval_state *ceval = &interp->ceval; |     struct _ceval_state *ceval = &interp->ceval; | ||||||
|     /* If tstate is NULL, the caller is indicating that we're releasing
 |     /* If final_release is true, the caller is indicating that we're releasing
 | ||||||
|        the GIL for the last time in this thread.  This is particularly |        the GIL for the last time in this thread.  This is particularly | ||||||
|        relevant when the current thread state is finalizing or its |        relevant when the current thread state is finalizing or its | ||||||
|        interpreter is finalizing (either may be in an inconsistent |        interpreter is finalizing (either may be in an inconsistent | ||||||
|        state).  In that case the current thread will definitely |        state).  In that case the current thread will definitely | ||||||
|        never try to acquire the GIL again. */ |        never try to acquire the GIL again. */ | ||||||
|     // XXX It may be more correct to check tstate->_status.finalizing.
 |     // XXX It may be more correct to check tstate->_status.finalizing.
 | ||||||
|     // XXX assert(tstate == NULL || !tstate->_status.cleared);
 |     // XXX assert(final_release || !tstate->_status.cleared);
 | ||||||
| 
 | 
 | ||||||
|  |     assert(final_release || tstate != NULL); | ||||||
|     struct _gil_runtime_state *gil = ceval->gil; |     struct _gil_runtime_state *gil = ceval->gil; | ||||||
| #ifdef Py_GIL_DISABLED | #ifdef Py_GIL_DISABLED | ||||||
|     if (!_Py_atomic_load_int_relaxed(&gil->enabled)) { |     // Check if we have the GIL before dropping it. tstate will be NULL if
 | ||||||
|  |     // take_gil() detected that this thread has been destroyed, in which case
 | ||||||
|  |     // we know we have the GIL.
 | ||||||
|  |     if (tstate != NULL && !tstate->_status.holds_gil) { | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| #endif | #endif | ||||||
|  | @ -238,26 +245,23 @@ drop_gil(PyInterpreterState *interp, PyThreadState *tstate) | ||||||
|         Py_FatalError("drop_gil: GIL is not locked"); |         Py_FatalError("drop_gil: GIL is not locked"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /* tstate is allowed to be NULL (early interpreter init) */ |     if (!final_release) { | ||||||
|     if (tstate != NULL) { |  | ||||||
|         /* Sub-interpreter support: threads might have been switched
 |         /* Sub-interpreter support: threads might have been switched
 | ||||||
|            under our feet using PyThreadState_Swap(). Fix the GIL last |            under our feet using PyThreadState_Swap(). Fix the GIL last | ||||||
|            holder variable so that our heuristics work. */ |            holder variable so that our heuristics work. */ | ||||||
|         _Py_atomic_store_ptr_relaxed(&gil->last_holder, tstate); |         _Py_atomic_store_ptr_relaxed(&gil->last_holder, tstate); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     drop_gil_impl(gil); |     drop_gil_impl(tstate, gil); | ||||||
| 
 | 
 | ||||||
| #ifdef FORCE_SWITCHING | #ifdef FORCE_SWITCHING | ||||||
|     /* We check tstate first in case we might be releasing the GIL for
 |     /* We might be releasing the GIL for the last time in this thread.  In that
 | ||||||
|        the last time in this thread.  In that case there's a possible |        case there's a possible race with tstate->interp getting deleted after | ||||||
|        race with tstate->interp getting deleted after gil->mutex is |        gil->mutex is unlocked and before the following code runs, leading to a | ||||||
|        unlocked and before the following code runs, leading to a crash. |        crash.  We can use final_release to indicate the thread is done with the | ||||||
|        We can use (tstate == NULL) to indicate the thread is done with |        GIL, and that's the only time we might delete the interpreter.  See | ||||||
|        the GIL, and that's the only time we might delete the |        https://github.com/python/cpython/issues/104341. */
 | ||||||
|        interpreter, so checking tstate first prevents the crash. |     if (!final_release && | ||||||
|        See https://github.com/python/cpython/issues/104341. */
 |  | ||||||
|     if (tstate != NULL && |  | ||||||
|         _Py_eval_breaker_bit_is_set(tstate, _PY_GIL_DROP_REQUEST_BIT)) { |         _Py_eval_breaker_bit_is_set(tstate, _PY_GIL_DROP_REQUEST_BIT)) { | ||||||
|         MUTEX_LOCK(gil->switch_mutex); |         MUTEX_LOCK(gil->switch_mutex); | ||||||
|         /* Not switched yet => wait */ |         /* Not switched yet => wait */ | ||||||
|  | @ -284,7 +288,7 @@ drop_gil(PyInterpreterState *interp, PyThreadState *tstate) | ||||||
|    tstate must be non-NULL. |    tstate must be non-NULL. | ||||||
| 
 | 
 | ||||||
|    Returns 1 if the GIL was acquired, or 0 if not. */ |    Returns 1 if the GIL was acquired, or 0 if not. */ | ||||||
| static int | static void | ||||||
| take_gil(PyThreadState *tstate) | take_gil(PyThreadState *tstate) | ||||||
| { | { | ||||||
|     int err = errno; |     int err = errno; | ||||||
|  | @ -309,7 +313,7 @@ take_gil(PyThreadState *tstate) | ||||||
|     struct _gil_runtime_state *gil = interp->ceval.gil; |     struct _gil_runtime_state *gil = interp->ceval.gil; | ||||||
| #ifdef Py_GIL_DISABLED | #ifdef Py_GIL_DISABLED | ||||||
|     if (!_Py_atomic_load_int_relaxed(&gil->enabled)) { |     if (!_Py_atomic_load_int_relaxed(&gil->enabled)) { | ||||||
|         return 0; |         return; | ||||||
|     } |     } | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
|  | @ -358,10 +362,10 @@ take_gil(PyThreadState *tstate) | ||||||
|     if (!_Py_atomic_load_int_relaxed(&gil->enabled)) { |     if (!_Py_atomic_load_int_relaxed(&gil->enabled)) { | ||||||
|         // Another thread disabled the GIL between our check above and
 |         // Another thread disabled the GIL between our check above and
 | ||||||
|         // now. Don't take the GIL, signal any other waiting threads, and
 |         // now. Don't take the GIL, signal any other waiting threads, and
 | ||||||
|         // return 0.
 |         // return.
 | ||||||
|         COND_SIGNAL(gil->cond); |         COND_SIGNAL(gil->cond); | ||||||
|         MUTEX_UNLOCK(gil->mutex); |         MUTEX_UNLOCK(gil->mutex); | ||||||
|         return 0; |         return; | ||||||
|     } |     } | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
|  | @ -393,20 +397,21 @@ take_gil(PyThreadState *tstate) | ||||||
|            in take_gil() while the main thread called |            in take_gil() while the main thread called | ||||||
|            wait_for_thread_shutdown() from Py_Finalize(). */ |            wait_for_thread_shutdown() from Py_Finalize(). */ | ||||||
|         MUTEX_UNLOCK(gil->mutex); |         MUTEX_UNLOCK(gil->mutex); | ||||||
|         /* Passing NULL to drop_gil() indicates that this thread is about to
 |         /* tstate could be a dangling pointer, so don't pass it to
 | ||||||
|            terminate and will never hold the GIL again. */ |            drop_gil(). */ | ||||||
|         drop_gil(interp, NULL); |         drop_gil(interp, NULL, 1); | ||||||
|         PyThread_exit_thread(); |         PyThread_exit_thread(); | ||||||
|     } |     } | ||||||
|     assert(_PyThreadState_CheckConsistency(tstate)); |     assert(_PyThreadState_CheckConsistency(tstate)); | ||||||
| 
 | 
 | ||||||
|  |     tstate->_status.holds_gil = 1; | ||||||
|     _Py_unset_eval_breaker_bit(tstate, _PY_GIL_DROP_REQUEST_BIT); |     _Py_unset_eval_breaker_bit(tstate, _PY_GIL_DROP_REQUEST_BIT); | ||||||
|     update_eval_breaker_for_thread(interp, tstate); |     update_eval_breaker_for_thread(interp, tstate); | ||||||
| 
 | 
 | ||||||
|     MUTEX_UNLOCK(gil->mutex); |     MUTEX_UNLOCK(gil->mutex); | ||||||
| 
 | 
 | ||||||
|     errno = err; |     errno = err; | ||||||
|     return 1; |     return; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void _PyEval_SetSwitchInterval(unsigned long microseconds) | void _PyEval_SetSwitchInterval(unsigned long microseconds) | ||||||
|  | @ -451,10 +456,17 @@ PyEval_ThreadsInitialized(void) | ||||||
| static inline int | static inline int | ||||||
| current_thread_holds_gil(struct _gil_runtime_state *gil, PyThreadState *tstate) | current_thread_holds_gil(struct _gil_runtime_state *gil, PyThreadState *tstate) | ||||||
| { | { | ||||||
|     if (((PyThreadState*)_Py_atomic_load_ptr_relaxed(&gil->last_holder)) != tstate) { |     int holds_gil = tstate->_status.holds_gil; | ||||||
|         return 0; | 
 | ||||||
|     } |     // holds_gil is the source of truth; check that last_holder and gil->locked
 | ||||||
|     return _Py_atomic_load_int_relaxed(&gil->locked); |     // are consistent with it.
 | ||||||
|  |     int locked = _Py_atomic_load_int_relaxed(&gil->locked); | ||||||
|  |     int is_last_holder = | ||||||
|  |         ((PyThreadState*)_Py_atomic_load_ptr_relaxed(&gil->last_holder)) == tstate; | ||||||
|  |     assert(!holds_gil || locked); | ||||||
|  |     assert(!holds_gil || is_last_holder); | ||||||
|  | 
 | ||||||
|  |     return holds_gil; | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
|  | @ -563,23 +575,24 @@ PyEval_ReleaseLock(void) | ||||||
|     /* This function must succeed when the current thread state is NULL.
 |     /* This function must succeed when the current thread state is NULL.
 | ||||||
|        We therefore avoid PyThreadState_Get() which dumps a fatal error |        We therefore avoid PyThreadState_Get() which dumps a fatal error | ||||||
|        in debug mode. */ |        in debug mode. */ | ||||||
|     drop_gil(tstate->interp, tstate); |     drop_gil(tstate->interp, tstate, 0); | ||||||
| } |  | ||||||
| 
 |  | ||||||
| int |  | ||||||
| _PyEval_AcquireLock(PyThreadState *tstate) |  | ||||||
| { |  | ||||||
|     _Py_EnsureTstateNotNULL(tstate); |  | ||||||
|     return take_gil(tstate); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void | void | ||||||
| _PyEval_ReleaseLock(PyInterpreterState *interp, PyThreadState *tstate) | _PyEval_AcquireLock(PyThreadState *tstate) | ||||||
| { | { | ||||||
|     /* If tstate is NULL then we do not expect the current thread
 |     _Py_EnsureTstateNotNULL(tstate); | ||||||
|        to acquire the GIL ever again. */ |     take_gil(tstate); | ||||||
|     assert(tstate == NULL || tstate->interp == interp); | } | ||||||
|     drop_gil(interp, tstate); | 
 | ||||||
|  | void | ||||||
|  | _PyEval_ReleaseLock(PyInterpreterState *interp, | ||||||
|  |                     PyThreadState *tstate, | ||||||
|  |                     int final_release) | ||||||
|  | { | ||||||
|  |     assert(tstate != NULL); | ||||||
|  |     assert(tstate->interp == interp); | ||||||
|  |     drop_gil(interp, tstate, final_release); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void | void | ||||||
|  | @ -1136,7 +1149,12 @@ _PyEval_DisableGIL(PyThreadState *tstate) | ||||||
|         //
 |         //
 | ||||||
|         // Drop the GIL, which will wake up any threads waiting in take_gil()
 |         // Drop the GIL, which will wake up any threads waiting in take_gil()
 | ||||||
|         // and let them resume execution without the GIL.
 |         // and let them resume execution without the GIL.
 | ||||||
|         drop_gil_impl(gil); |         drop_gil_impl(tstate, gil); | ||||||
|  | 
 | ||||||
|  |         // If another thread asked us to drop the GIL, they should be
 | ||||||
|  |         // free-threading by now. Remove any such request so we have a clean
 | ||||||
|  |         // slate if/when the GIL is enabled again.
 | ||||||
|  |         _Py_unset_eval_breaker_bit(tstate, _PY_GIL_DROP_REQUEST_BIT); | ||||||
|         return 1; |         return 1; | ||||||
|     } |     } | ||||||
|     return 0; |     return 0; | ||||||
|  |  | ||||||
|  | @ -1843,7 +1843,7 @@ _PyThreadState_DeleteCurrent(PyThreadState *tstate) | ||||||
| #endif | #endif | ||||||
|     current_fast_clear(tstate->interp->runtime); |     current_fast_clear(tstate->interp->runtime); | ||||||
|     tstate_delete_common(tstate); |     tstate_delete_common(tstate); | ||||||
|     _PyEval_ReleaseLock(tstate->interp, NULL); |     _PyEval_ReleaseLock(tstate->interp, tstate, 1); | ||||||
|     free_threadstate((_PyThreadStateImpl *)tstate); |     free_threadstate((_PyThreadStateImpl *)tstate); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -2068,7 +2068,7 @@ _PyThreadState_Attach(PyThreadState *tstate) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     while (1) { |     while (1) { | ||||||
|         int acquired_gil = _PyEval_AcquireLock(tstate); |         _PyEval_AcquireLock(tstate); | ||||||
| 
 | 
 | ||||||
|         // XXX assert(tstate_is_alive(tstate));
 |         // XXX assert(tstate_is_alive(tstate));
 | ||||||
|         current_fast_set(&_PyRuntime, tstate); |         current_fast_set(&_PyRuntime, tstate); | ||||||
|  | @ -2079,20 +2079,17 @@ _PyThreadState_Attach(PyThreadState *tstate) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| #ifdef Py_GIL_DISABLED | #ifdef Py_GIL_DISABLED | ||||||
|         if (_PyEval_IsGILEnabled(tstate) != acquired_gil) { |         if (_PyEval_IsGILEnabled(tstate) && !tstate->_status.holds_gil) { | ||||||
|             // The GIL was enabled between our call to _PyEval_AcquireLock()
 |             // The GIL was enabled between our call to _PyEval_AcquireLock()
 | ||||||
|             // and when we attached (the GIL can't go from enabled to disabled
 |             // and when we attached (the GIL can't go from enabled to disabled
 | ||||||
|             // here because only a thread holding the GIL can disable
 |             // here because only a thread holding the GIL can disable
 | ||||||
|             // it). Detach and try again.
 |             // it). Detach and try again.
 | ||||||
|             assert(!acquired_gil); |  | ||||||
|             tstate_set_detached(tstate, _Py_THREAD_DETACHED); |             tstate_set_detached(tstate, _Py_THREAD_DETACHED); | ||||||
|             tstate_deactivate(tstate); |             tstate_deactivate(tstate); | ||||||
|             current_fast_clear(&_PyRuntime); |             current_fast_clear(&_PyRuntime); | ||||||
|             continue; |             continue; | ||||||
|         } |         } | ||||||
|         _Py_qsbr_attach(((_PyThreadStateImpl *)tstate)->qsbr); |         _Py_qsbr_attach(((_PyThreadStateImpl *)tstate)->qsbr); | ||||||
| #else |  | ||||||
|         (void)acquired_gil; |  | ||||||
| #endif | #endif | ||||||
|         break; |         break; | ||||||
|     } |     } | ||||||
|  | @ -2123,7 +2120,7 @@ detach_thread(PyThreadState *tstate, int detached_state) | ||||||
|     tstate_deactivate(tstate); |     tstate_deactivate(tstate); | ||||||
|     tstate_set_detached(tstate, detached_state); |     tstate_set_detached(tstate, detached_state); | ||||||
|     current_fast_clear(&_PyRuntime); |     current_fast_clear(&_PyRuntime); | ||||||
|     _PyEval_ReleaseLock(tstate->interp, tstate); |     _PyEval_ReleaseLock(tstate->interp, tstate, 0); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void | void | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Brett Simmers
						Brett Simmers