Skip to content

gh-59705: Add _thread.set_name() function #127338

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c6d324d
gh-59705: Add _thread.set_name() function
vstinner Nov 27, 2024
63b5d52
Port to macOS
vstinner Nov 27, 2024
9f6a8ab
Add tests
vstinner Nov 27, 2024
d79e7af
Try to fix macOS _get_name()
vstinner Nov 27, 2024
ebd9752
Truncate to 15 bytes; add error handling
vstinner Nov 28, 2024
a7f5651
Address review
vstinner Nov 28, 2024
97ea645
Add test on non-ASCII name truncation
vstinner Nov 28, 2024
78a9ab9
Add test on non-ASCII name
vstinner Nov 28, 2024
dcf13f4
Test long name on non-Linux platforms
vstinner Nov 28, 2024
6ea7e5a
macOS is limited to 63 bytes
vstinner Nov 28, 2024
46721bb
Catch UnicodeEncodeError when seting the name
vstinner Nov 28, 2024
6962116
Add tests
vstinner Nov 28, 2024
5d27da0
Use "replace" error handler
vstinner Nov 28, 2024
b713910
Address review
vstinner Nov 29, 2024
5c20ea1
Use PyInterpreterState filesystem encoding
vstinner Nov 29, 2024
6088b37
FreeBSD truncates to 98 bytes silently
vstinner Dec 2, 2024
3c12d17
Merge branch 'main' into thread_set_name
vstinner Dec 3, 2024
bde935f
Fix test_threading on iOS
vstinner Dec 3, 2024
0584ca3
Fix create_test(): always encode using "replace"
vstinner Dec 3, 2024
7508b6c
Solaris truncates to 31 bytes
vstinner Dec 3, 2024
f4a9f40
Solaris always uses UTF-8
vstinner Dec 4, 2024
ac6d726
Add PYTHREAD_NAME_MAXLEN macro
vstinner Dec 4, 2024
08e5922
Fix configure.ac if PYTHREAD_NAME_MAXLEN is not set
vstinner Dec 4, 2024
681624e
Address Serhiy's review
vstinner Dec 5, 2024
f57339f
Solaris always use UTF-8
vstinner Dec 6, 2024
3941123
Simplify tests
vstinner Dec 6, 2024
2e27043
Use @unittest.skipUnless
vstinner Dec 6, 2024
6ab2944
Solaris uses UTF-8
vstinner Dec 6, 2024
b370c49
Update Lib/test/test_threading.py
vstinner Dec 6, 2024
beeae59
add test on embedded null character
vstinner Dec 6, 2024
ae956a0
Optimize code truncating the name
vstinner Dec 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions Lib/test/test_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -2103,6 +2103,58 @@ def test__all__(self):
support.check__all__(self, threading, ('threading', '_thread'),
extra=extra, not_exported=not_exported)

@unittest.skipUnless(hasattr(_thread, 'set_name'), "missing _thread.set_name")
@unittest.skipUnless(hasattr(_thread, '_get_name'), "missing _thread._get_name")
def test_set_name(self):
# set_name() limit in bytes
truncate = getattr(_thread, "_NAME_MAXLEN", None)
limit = truncate or 100

tests = [
# test short ASCII name
"CustomName",

# test short non-ASCII name
"namé€",

# Test long ASCII names (not truncated)
"x" * limit,

# Test long ASCII names (truncated)
"x" * (limit + 10),

# Test long non-ASCII name (truncated)
"x" * (limit - 1) + "é€",
]
if os_helper.FS_NONASCII:
tests.append(f"nonascii:{os_helper.FS_NONASCII}")
if os_helper.TESTFN_UNENCODABLE:
tests.append(os_helper.TESTFN_UNENCODABLE)

if sys.platform.startswith("solaris"):
encoding = "utf-8"
else:
encoding = sys.getfilesystemencoding()

def work():
nonlocal work_name
work_name = _thread._get_name()

for name in tests:
encoded = name.encode(encoding, "replace")
if truncate is not None:
expected = os.fsdecode(encoded[:truncate])
else:
expected = os.fsdecode(encoded)

with self.subTest(name=name, expected=expected):
work_name = None
thread = threading.Thread(target=work, name=name)
thread.start()
thread.join()
self.assertEqual(work_name, expected,
f"{len(work_name)=} and {len(expected)=}")


class InterruptMainTests(unittest.TestCase):
def check_interrupt_main_with_signal_handler(self, signum):
Expand Down
9 changes: 9 additions & 0 deletions Lib/threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
__all__.append('get_native_id')
except AttributeError:
_HAVE_THREAD_NATIVE_ID = False
try:
_set_name = _thread.set_name
except AttributeError:
_set_name = None
ThreadError = _thread.error
try:
_CRLock = _thread.RLock
Expand Down Expand Up @@ -1027,6 +1031,11 @@ def _bootstrap_inner(self):
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._started.set()
with _active_limbo_lock:
_active[self._ident] = self
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
On Linux, :class:`threading.Thread` now sets the thread name to the
operating system. Patch by Victor Stinner.
107 changes: 107 additions & 0 deletions Modules/_threadmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
# include <signal.h> // SIGINT
#endif

#include "clinic/_threadmodule.c.h"

// ThreadError is just an alias to PyExc_RuntimeError
#define ThreadError PyExc_RuntimeError

Expand Down Expand Up @@ -44,6 +46,13 @@ get_thread_state(PyObject *module)
return (thread_module_state *)state;
}


/*[clinic input]
module _thread
[clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=be8dbe5cc4b16df7]*/


// _ThreadHandle type

// Handles state transitions according to the following diagram:
Expand Down Expand Up @@ -2354,6 +2363,95 @@ PyDoc_STRVAR(thread__get_main_thread_ident_doc,
Internal only. Return a non-zero integer that uniquely identifies the main thread\n\
of the main interpreter.");


#ifdef HAVE_PTHREAD_GETNAME_NP
/*[clinic input]
_thread._get_name

Get the name of the current thread.
[clinic start generated code]*/

static PyObject *
_thread__get_name_impl(PyObject *module)
/*[clinic end generated code: output=20026e7ee3da3dd7 input=35cec676833d04c8]*/
{
// Linux and macOS are limited to respectively 16 and 64 bytes
char name[100];
pthread_t thread = pthread_self();
int rc = pthread_getname_np(thread, name, Py_ARRAY_LENGTH(name));
if (rc) {
errno = rc;
return PyErr_SetFromErrno(PyExc_OSError);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pthread_getname_np should add a trailing NUL byte, but like everything here, that's platform-specific. I suggest being defensive here.

Suggested change
name[Py_ARRAY_LENGTH(name)-1] = 0;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On what platform it does not add the null byte?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The null byte is added on all supported platforms. Before I made sure that the buffer always ended with a null byte, but @serhiy-storchaka asked me to remove it. Let's be optimistic. We can adjust the code later if needed.

#ifdef __sun
return PyUnicode_DecodeUTF8(name, strlen(name), "surrogateescape");
#else
return PyUnicode_DecodeFSDefault(name);
#endif
}
#endif // HAVE_PTHREAD_GETNAME_NP


#ifdef HAVE_PTHREAD_SETNAME_NP
/*[clinic input]
_thread.set_name

name as name_obj: unicode

Set the name of the current thread.
[clinic start generated code]*/

static PyObject *
_thread_set_name_impl(PyObject *module, PyObject *name_obj)
/*[clinic end generated code: output=402b0c68e0c0daed input=7e7acd98261be82f]*/
{
#ifdef __sun
// Solaris always uses UTF-8
const char *encoding = "utf-8";
#else
// Encode the thread name to the filesystem encoding using the "replace"
// error handler
PyInterpreterState *interp = _PyInterpreterState_GET();
const char *encoding = interp->unicode.fs_codec.encoding;
#endif
PyObject *name_encoded;
name_encoded = PyUnicode_AsEncodedString(name_obj, encoding, "replace");
if (name_encoded == NULL) {
return NULL;
}

const char *name = PyBytes_AS_STRING(name_encoded);
#ifdef PYTHREAD_NAME_MAXLEN
// Truncate to PYTHREAD_NAME_MAXLEN bytes + the NUL byte if needed
size_t len = strlen(name);
if (len > PYTHREAD_NAME_MAXLEN) {
PyObject *truncated = PyBytes_FromStringAndSize(name, PYTHREAD_NAME_MAXLEN);
if (truncated == NULL) {
Py_DECREF(name_encoded);
return NULL;
}
Py_SETREF(name_encoded, truncated);
name = PyBytes_AS_STRING(name_encoded);
}
#endif

#ifdef __APPLE__
int rc = pthread_setname_np(name);
#else
pthread_t thread = pthread_self();
int rc = pthread_setname_np(thread, name);
#endif
Py_DECREF(name_encoded);
if (rc) {
errno = rc;
return PyErr_SetFromErrno(PyExc_OSError);
}
Py_RETURN_NONE;
}
#endif // HAVE_PTHREAD_SETNAME_NP


static PyMethodDef thread_methods[] = {
{"start_new_thread", (PyCFunction)thread_PyThread_start_new_thread,
METH_VARARGS, start_new_thread_doc},
Expand Down Expand Up @@ -2393,6 +2491,8 @@ static PyMethodDef thread_methods[] = {
METH_O, thread__make_thread_handle_doc},
{"_get_main_thread_ident", thread__get_main_thread_ident,
METH_NOARGS, thread__get_main_thread_ident_doc},
_THREAD_SET_NAME_METHODDEF
_THREAD__GET_NAME_METHODDEF
{NULL, NULL} /* sentinel */
};

Expand Down Expand Up @@ -2484,6 +2584,13 @@ thread_module_exec(PyObject *module)

llist_init(&state->shutdown_handles);

#ifdef PYTHREAD_NAME_MAXLEN
if (PyModule_AddIntConstant(module, "_NAME_MAXLEN",
PYTHREAD_NAME_MAXLEN) < 0) {
return -1;
}
#endif

return 0;
}

Expand Down
104 changes: 104 additions & 0 deletions Modules/clinic/_threadmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions configure

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading