_Py_ThreadId and __readgsqword(48) on Windows x64

@colesbury in your Implement biased reference counting commit you introduce the new _Py_ThreadId(), and on Windows 64-bit you’re returning __readgsqword(48), which is the offset that corresponds to FIELD_OFFSET(NT_TIB, Self), so, basically, the address of struct _NT_TIB, which is fine for a 64-bit unique thread identifier, but I’m curious why you didn’t prefer the actual thread ID, available as a 32-bit offset from __readgsdword(0x48) (72 decimal)?

Having the actual thread ID embedded within ob_tid could be a useful breadcrumb when debugging some types of gnarly issues. On the flip side, I guess with the NT_TIB address you don’t even need to consider any temporal duplicate thread ID issues—so my hunch is that that’s the reason, but I just wanted to double-check.

(Context: writing an article regarding free-threading stuff, was reviewing PyParallel, which did __readgsdword(0x48), looked at CPython and saw __readgsqword(48) and was like “Oh neat, they’re doing the same thing!”… only to realize that you’re doing a qword read and from offset 48 decimal not 0x48. Much confusion ensued until I realized FIELD_OFFSET(NT_TIB, Self) == 48 (0x30).)

1 Like

Pretty sure the TIB address is just as reusable as a thread ID.

I’d agree that having the actual ID would be better if it’s possible, for debugging reasons as you say. I’m not aware of the underlying reasons for preferring the address (if any).

1 Like

Yeah TIB address is absolutely usable as a unique thread identifier. It just doesn’t have any correlation whatsoever to the Win32 thread ID plumbed through to all other layers of the stack :slight_smile:

Figuring out which thread ID corresponds to the TIB address captured in op->ob_tid would be fiddly if using Windbg, not sure if you could do some casting-fu in the Visual Studio debugger’s immediate window to achieve the same effect.

Steve and I prefer switching to the actual thread ID via __readgsdword(0x48) on 64-bit (0x24 on 32-bit).

Any advocates for sticking with the TIB address because of something we’ve overlooked that want to chime in?

1 Like

No, don’t change it. ob_tid is pointer sized. The TEB address is pointer sized. The thread id is 4 bytes. In some contexts, it’s faster to compare a pointer to a pointer than a pointer to a sign or zero-extended 4 bytes.

EDIT: I forgot about another more important reason: ob_tid must be a pointer – it must be distinct from any other object for as long as the thread is alive. The GC temporarily repurposes ob_tid for a linked list and it’s important that no thread id matches the linked list pointers.

You can also get the thread ID from the TEB with something like: ((int *)ob_tid)[18] assuming the thread is still alive (it may not be – objects can outlive the thread that created them).