You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Thread spuriously marked dead after interrupting a join call #90882
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
assignee=Noneclosed_at=Nonecreated_at=<Date2022-02-11.23:47:13.610>labels= ['type-bug', 'library', '3.9', '3.10', '3.11']
title='Thread spuriously marked dead after interrupting a join call'updated_at=<Date2022-02-14.02:08:58.437>user='https://bugs.python.org/KevinShweh'
try:
if lock.acquire(block, timeout):
lock.release()
self._stop()
except:
if lock.locked():
# bpo-45274: lock.acquire() acquired the lock, but the function
# was interrupted with an exception before reaching the
# lock.release(). It can happen if a signal handler raises an
# exception, like CTRL+C which raises KeyboardInterrupt.
lock.release()
self._stop()
raise
has a bug. The "if lock.locked()" check doesn't check whether this code managed to acquire the lock. It checks if *anyone at all* is holding the lock. The lock is almost always locked, so this code will perform a spurious call to self._stop() if it gets interrupted while trying to acquire the lock.
Thread.join uses this method to wait for a thread to finish, so a thread will spuriously be marked dead if you interrupt a join call with Ctrl-C while it's trying to acquire the lock. Here's a reproducer:
I've just added a PR there (or should it go here?) that (i think) fixes this.
The issue is that the lock.locked() call just checks that *someone* has the lock, not that the previous acquire() is what got the lock.
If it's just that the tstate lock is held because the thread is still running, then it's premature to release() the lock.
KevinShweh commented on Feb 12, 2022
KevinShwehmannequin
MannequinAuthor
More actions
bpo-45274 was a subtly different issue. That was a problem that happened if the thread got interrupted *between* the acquire and the release, causing it to *not* release the lock and *not* perform end-of-thread cleanup.
The fix for that issue caused this issue, which happens if the thread gets interrupted *during* the acquire, in which case it *does* release the lock (that someone else is holding) and *does* perform end-of-thread cleanup even though it's not supposed to do either of those things.
Reacted by Abraham Murciano
KevinShweh commented on Feb 12, 2022
KevinShwehmannequin
MannequinAuthor
More actions
The PR you submitted doesn't work, unfortunately. It essentially reintroduces bpo-45274. If this line:
iflocked:=lock.acquire(block, timeout):
gets interrupted between the acquire and the assignment, locked is still False. That's rare, but so is an interruption between the acquire and the release, which is the original form of bpo-45274.
bensimner commented on Feb 12, 2022
bensimnermannequin
Mannequin
More actions
You are right,
What one really needs here is a way to know *who* owns the lock,
but threading.Lock does not provide that.
The race on := is much smaller than the original race and I suspect in practice will be very hard to hit.
As the original bpo notes, it may not be possible to write a complete fix for this in pure Python, after all there may always be another interrupt between the except and the second attempted release.
The issue with the solution was that it turned a relatively hard-to-hit race condition into a really easy-to-hit one, but perhaps the outcome is slightly less worse?
Activity
KevinShweh commentedon Feb 11, 2022
This code in Thread._wait_for_tstate_lock:
has a bug. The "if lock.locked()" check doesn't check whether this code managed to acquire the lock. It checks if *anyone at all* is holding the lock. The lock is almost always locked, so this code will perform a spurious call to self._stop() if it gets interrupted while trying to acquire the lock.
Thread.join uses this method to wait for a thread to finish, so a thread will spuriously be marked dead if you interrupt a join call with Ctrl-C while it's trying to acquire the lock. Here's a reproducer:
Interrupt this code with Ctrl-C during the join(), and print(t.is_alive()) will print False.
bensimner commentedon Feb 12, 2022
This is a duplicate of https://bugs.python.org/issue45274
but the patch there did not fix it
I've just added a PR there (or should it go here?) that (i think) fixes this.
The issue is that the lock.locked() call just checks that *someone* has the lock, not that the previous acquire() is what got the lock.
If it's just that the tstate lock is held because the thread is still running, then it's premature to release() the lock.
KevinShweh commentedon Feb 12, 2022
bpo-45274 was a subtly different issue. That was a problem that happened if the thread got interrupted *between* the acquire and the release, causing it to *not* release the lock and *not* perform end-of-thread cleanup.
The fix for that issue caused this issue, which happens if the thread gets interrupted *during* the acquire, in which case it *does* release the lock (that someone else is holding) and *does* perform end-of-thread cleanup even though it's not supposed to do either of those things.
KevinShweh commentedon Feb 12, 2022
The PR you submitted doesn't work, unfortunately. It essentially reintroduces bpo-45274. If this line:
gets interrupted between the acquire and the assignment, locked is still False. That's rare, but so is an interruption between the acquire and the release, which is the original form of bpo-45274.
bensimner commentedon Feb 12, 2022
You are right,
What one really needs here is a way to know *who* owns the lock,
but threading.Lock does not provide that.
The race on := is much smaller than the original race and I suspect in practice will be very hard to hit.
As the original bpo notes, it may not be possible to write a complete fix for this in pure Python, after all there may always be another interrupt between the
exceptand the second attemptedrelease.The issue with the solution was that it turned a relatively hard-to-hit race condition into a really easy-to-hit one, but perhaps the outcome is slightly less worse?
22 remaining items
fix(debug_ext): CTRL+C handling while waiting on gdb process
fix(debug_ext): CTRL+C handling while waiting on gdb process
[wptrunner] Ensure `ManagerGroup.wait()` waits for all threads to stop
[wptrunner] Ensure `ManagerGroup.wait()` waits for all threads to stop
[wptrunner] Ensure `ManagerGroup.wait()` waits for all threads to stop (
Bug 1890086 [wpt PR 45593] - [wptrunner] Ensure `ManagerGroup.wait()`…
abrahammurciano commentedon May 21, 2024
In the 3.13 branch the implementation of
Thread.joinis just a wrapper around_thread._ThreadHandle.join, which is implemented in C.https://github.com/python/cpython/blob/3.13/Lib/threading.py#L1090
Does this mean this issue will be fixed by this change in python3.13?
fofoni commentedon May 22, 2024
Looks like it's really fixed! Just tested the reproducer in the OP:
and:
(Note that this bug was really introduced only in 3.9.)
vstinner commentedon Mar 5, 2025
I close the issue, it has been fixed in Python 3.13.
I confirm, I also see "True" (alive) when I interrupt the reproducer with CTRL+C on Python 3.13 and 3.14.
Python 3.12 has still the issue, but I consider that the bug is a minor issue.
Bug 1890086 [wpt PR 45593] - [wptrunner] Ensure `ManagerGroup.wait()`…
fix(file-agent): let an interrupt reach a wait that has no deadline
fix(file-agent): let an interrupt reach a wait that has no deadline
fix(file-agent): let an interrupt reach a wait that has no deadline