Change:
} thread_local! { <ins> // Invariant: `CURRENT` and `CURRENT_ID` will always be initialized together. // If `CURRENT` is initialized, then `CURRENT_ID` will hold the same value // as `CURRENT.id()`. </ins> static CURRENT: OnceCell<Thread> = const { OnceCell::new() }; <ins> static CURRENT_ID: Cell<Option<ThreadId>> = const { Cell::new(None) }; </ins> } /// Sets the thread handle for the current thread. /// /// Aborts if the handle has been set already to reduce code size. pub(crate) fn set_current(thread: Thread) { <ins> let tid = thread.id(); </ins> // Using `unwrap` here can add ~3kB to the binary size. We have complete // control over where this is called, so just abort if there is a bug. CURRENT.with(|current| match current.set(thread) { <del> Ok(()) => {} </del> <ins> Ok(()) => CURRENT_ID.set(Some(tid)), </ins> Err(_) => rtabort!("thread::set_current should only be called once per thread"), }); }

Discussion:
The code below verifies that the only one thread holds the ReentrantLock by recording the owner thread id inside. It should be true since the lock is never unlocked (note that the lock guard is leaked). In practice, in some executions the assertion fails: Inspired by report from , which also explains why this fails. Ideally the assertion failure wouldn't be among possible executions.
This is a bit of rule-lawyering, but this isn't guaranteed. The documentation of says: And all of these things are fulfilled, even in the case you describe.
That part is false, since according to the documentation lock is unlocked when a lock guard is dropped. No lock guard is ever dropped in the example, so another thread still holds the lock. If you would like to allow this execution, you would have to update the documentation to explain when and why lock was unlocked. Although, I would consider this to be clearly undesirable behavior. Furthermore we already have ThreadId with necessary guarantees could be used to address the issue.
No, the other thread has died. Unfortunately, getting the is much more expensive, as it lives in , which is , and therefore needs a TLS destructor. Also, it will not be available in TLS destructors. We can get around this, of course, but the current approach is quite efficient, so it will be hard to beat.
I agree that if this behavior stays as is, it should at least be very explicitly documented (something like "If a thread previously held the lock and terminated without releasing it, another thread may then be able to acquire the lock without deadlocking"). Still I'd prefer for the semantics to be "If any thread terminates while holding a lock, no other thread may ever reacquire the lock", which would require something like which is unique over the lifetime of the process. I'm not convinced that the approach would be much slower than the current one, it's mostly just cloning and then dropping an Arc, which is 2 atomic ops, and it saves a library call. On the other hand, using would then always require a 64 bit ID, which might need some form of spin lock protecting every access on platforms that don't have 64 bit atomics.
I know the point of is to avoid deadlock, but my thinking is that if someone uses on the guard, they probably had a good reason for it, and might want to keep it locked.

I'm kinda fighting my own bias here though, since I've also been trying to prevent deadlocks in Rust.
As this seems important to so many people, we should definitely add those docs. I'm opposed to fixing this however, unless we find a really efficient solution. This gets called on every single , so it's definitely on the hot path. Also, we might consider using for this in the future, which might be more efficient on Linux than the TLS access. s can get reused, so if we fix this, we wouldn't be able to do that.
Modulo the need to use 64-bit atomics, a approach can be made almost entirely identical in performance to the current one: in when the current thread threadlocal, also have a thread local that only stores the thread ID and is set when the current thread is initialized and claims a thread ID. is replaced with consuming something like the following from : The common case -- the thread was created by Rust and has its data already created -- only gains a pointer read and predicted comparison against zero over the current which uses the thread local address.
Since the tid is only ever written to while holding a mutex, we could just use a seqlock to store it on platforms which only support 32-bit atomics (which I believe all platforms with do), something like this: . While this means at least 4 atomic operations per tid access (including a store) instead of just 1, this seems acceptable to me because it would only affect 32 bit platforms anyways, the majority of platforms could just use a single 64 bit atomic access. Also, reader-writer contention would be extremely low, as the tid only gets written to when a thread acquires the first lock and when the last lock gets released. Would this together with the more efficient fetching by be a reasonable tradeoff to allow the stricter guarantee for process-lifetime unique thread ids?
So the answer here is that the lock gets implicitly unlocked when the thread dies. This is not how works, but doesn't seem outrageous either. Of course if it can be avoided at reasonable cost, that seems better. I was about to remark on the unsoundness of seqlocks, but this variant actually seems fine -- you are using relaxed atomic accesses for the data, so there's no data race, just a race condition. Nice!

Note that it only looks unlocked to a subsequent thread if that thread is assigned the same TLS block as the thread that held the lock. Any other thread would still deadlock, which means we currently can't guarantee either behavior consistently.
Sure, this behavior is non-deterministic. I was just talking about the specific execution we are considering, where the second thread does acquire the lock.