Skip to content

Commit

Permalink
pythongh-59705: Set OS thread name when Thread.name is changed (pytho…
Browse files Browse the repository at this point in the history
…n#127702)

Co-authored-by: Petr Viktorin <[email protected]>
  • Loading branch information
vstinner and encukou authored Dec 10, 2024
1 parent 9af96f4 commit c91ccbe
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 7 deletions.
12 changes: 12 additions & 0 deletions Doc/library/threading.rst
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,18 @@ since it is impossible to detect the termination of alien threads.
Multiple threads may be given the same name. The initial name is set by
the constructor.

On some platforms, the thread name is set at the operating system level
when the thread starts, so that it is visible in task managers.
This name may be truncated to fit in a system-specific limit (for example,
15 bytes on Linux or 63 bytes on macOS).

Changes to *name* are only reflected at the OS level when the currently
running thread is renamed. (Setting the *name* attribute of a
different thread only updates the Python Thread object.)

.. versionchanged:: 3.14
Set the operating system thread name.

.. method:: getName()
setName()

Expand Down
19 changes: 19 additions & 0 deletions Lib/test/test_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -2164,6 +2164,25 @@ def work():
self.assertEqual(work_name, expected,
f"{len(work_name)=} and {len(expected)=}")

@unittest.skipUnless(hasattr(_thread, 'set_name'), "missing _thread.set_name")
@unittest.skipUnless(hasattr(_thread, '_get_name'), "missing _thread._get_name")
def test_change_name(self):
# Change the name of a thread while the thread is running

name1 = None
name2 = None
def work():
nonlocal name1, name2
name1 = _thread._get_name()
threading.current_thread().name = "new name"
name2 = _thread._get_name()

thread = threading.Thread(target=work, name="name")
thread.start()
thread.join()
self.assertEqual(name1, "name")
self.assertEqual(name2, "new name")


class InterruptMainTests(unittest.TestCase):
def check_interrupt_main_with_signal_handler(self, signum):
Expand Down
16 changes: 11 additions & 5 deletions Lib/threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -1026,16 +1026,20 @@ def _set_ident(self):
def _set_native_id(self):
self._native_id = get_native_id()

def _set_os_name(self):
if _set_name is None or not self._name:
return
try:
_set_name(self._name)
except OSError:
pass

def _bootstrap_inner(self):
try:
self._set_ident()
if _HAVE_THREAD_NATIVE_ID:
self._set_native_id()
if _set_name is not None and self._name:
try:
_set_name(self._name)
except OSError:
pass
self._set_os_name()
self._started.set()
with _active_limbo_lock:
_active[self._ident] = self
Expand Down Expand Up @@ -1115,6 +1119,8 @@ def name(self):
def name(self, name):
assert self._initialized, "Thread.__init__() not called"
self._name = str(name)
if get_ident() == self._ident:
self._set_os_name()

@property
def ident(self):
Expand Down
3 changes: 1 addition & 2 deletions Modules/_threadmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2423,8 +2423,7 @@ _thread_set_name_impl(PyObject *module, PyObject *name_obj)

#ifdef PYTHREAD_NAME_MAXLEN
// Truncate to PYTHREAD_NAME_MAXLEN bytes + the NUL byte if needed
size_t len = PyBytes_GET_SIZE(name_encoded);
if (len > PYTHREAD_NAME_MAXLEN) {
if (PyBytes_GET_SIZE(name_encoded) > PYTHREAD_NAME_MAXLEN) {
PyObject *truncated;
truncated = PyBytes_FromStringAndSize(PyBytes_AS_STRING(name_encoded),
PYTHREAD_NAME_MAXLEN);
Expand Down

0 comments on commit c91ccbe

Please sign in to comment.